ART-379 · Form Bubbles · desktop/mobile × ποδόσφαιρο/μπάσκετ · MVP: χωρίς κλικ ανά αγώνα στα mobile modals — scroll ↓

Form Bubbles

Η πρόσφατη φόρμα μιας ομάδας: κύκλοι που επικαλύπτονται, με το γράμμα του αποτελέσματος μέσα (Ν / Ι / Η) και ? για τον επόμενο αγώνα. Τέσσερις παραλλαγές — desktop / mobile × ποδόσφαιρο / μπάσκετ. Η γεωμετρία είναι κοινή· αλλάζει μόνο η συμπεριφορά και η λεπτομέρεια του αθλήματος. Κάνε κλικ (και hover στα desktop) σε όλα.

MVP scope

Για το MVP δεν χρειάζεται το κλικ ανά αγώνα μέσα στα mobile modals. Αρκεί το modal να ανοίγει και να εμφανίζει τους 5 αγώνες — χωρίς την ανάπτυξη της γραμμής όταν πατηθεί.

Δηλαδή εκτός MVP: το timeline γκολ/καρτών στο mobile ποδόσφαιρο (§3A) και η ανάλυση περιόδων Q1–Q4 στο mobile μπάσκετ (§3B). Είναι υλοποιημένα και λειτουργούν εδώ, αλλά μπορούν να μπουν σε επόμενο κύκλο. Τα desktop popovers δεν επηρεάζονται.

desktop football

variant=desktop · sport=football

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

mobile football

variant=mobile · sport=football

Φωτεινή επιφάνεια (πίνακας βαθμολογίας). Tap σε οποιοδήποτε bubble ανοίγει το .fdmodal με όλους τους αγώνες — όχι popover σε hover. Ανοίγει με όλες τις γραμμές κλειστές· πάτα μια γραμμή και ανοίγει το timeline 0–90′ με γκολ και κάρτες (ref 2b).

desktop basketball

variant=desktop · sport=basket

Ίδιο μοτίβο με το desktop football. Το popover δείχνει έδρα · αντίπαλο, σκορ, αποτέλεσμα + διαφορά και τον κορυφαίο παίκτη — χωρίς ανάλυση περιόδων. Η ανά-περίοδο πληροφορία υπάρχει μόνο στο mobile modal. Το τρίτο bubble είναι ο αγώνας-καμπή ΠΑΟ 84-82 ΠΑΟΚ.

mobile basketball

variant=mobile · sport=basket

Ίδιο modal με το mobile football. Ανοίγει με όλες τις γραμμές κλειστές· πάτα μια γραμμή και ανοίγει σε ανάλυση Q1–Q4: διαφορά περιόδου, μπάρες σκορ, και «περίοδοι που κερδήθηκαν». Πάτα άλλη για να μεταφερθεί η ανάλυση.

Design → Dev Handover

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

MVP scope

Για το MVP δεν χρειάζεται το κλικ ανά αγώνα μέσα στα mobile modals. Αρκεί το modal να ανοίγει και να εμφανίζει τους 5 αγώνες σε λίστα — χωρίς την ανάπτυξη της γραμμής όταν πατηθεί.

Εκτός MVP: §3A timeline γκολ/καρτών (mobile ποδόσφαιρο) · §3B ανάλυση περιόδων Q1–Q4 (mobile μπάσκετ). Και τα δύο είναι υλοποιημένα και δουλεύουν στο πρωτότυπο πιο πάνω, οπότε μπαίνουν όποτε θέλουμε — δεν είναι blocker.

Στο MVP μένουν: τα bubbles και οι 4 παραλλαγές, το modal με τους 5 αγώνες (σκορ, έδρα, ημερομηνία), και τα desktop popovers όπως είναι.

Assets

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

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

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

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

01Οι τέσσερις παραλλαγές

Δύο ανεξάρτητοι άξονες στο .formdots: data-variant (mobile / desktop) × data-sport (football / basket, default football). Ο παρακάτω πίνακας είναι ο άξονας data-variant· η διαφορά του αθλήματος είναι στο 3B.

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>

ΔΟΧΕΙΟ ΠΟΥ ΚΟΒΕΙ ΤΟ POPOVER

<!-- ΚΑΘΕ γονιός με overflow hidden/auto/scroll κόβει το popover.
     Βάλε data-fb-scroll στο wrapper και το .fpop γίνεται position:fixed
     ώστε να ξεφύγει. Ισχύει για πίνακες με scroll ΚΑΙ για κάρτες με
     overflow:hidden — π.χ. τα demo αυτής της σελίδας. -->
<div class="standwrap" data-fb-scroll> <table>…</table> </div>

3AMobile modal — ανάπτυξη γραμμής εκτός MVP

Το modal ανοίγει με όλες τις γραμμές κλειστές, και στα δύο αθλήματα. Πάτημα σε γραμμή της δίνει .is-open και εμφανίζει από κάτω τη λεπτομέρεια· πάτημα σε άλλη τη μεταφέρει, πάτημα στην ίδια την κλείνει. Μια γραμμή είναι αναπτύξιμη μόνο αν έχει δεδομένα: q για μπάσκετ, e για ποδόσφαιρο.

ΠΟΔΟΣΦΑΙΡΟ — TIMELINE ΓΚΟΛ & ΚΑΡΤΩΝ ref 2b

ΣτοιχείοΚλάσηΠεριεχόμενο
Γραμμή αποτελέσματος.fdacc-resτελεία + «Νίκη · Εκτός». Τελεία: νίκη var(--win) · ισοπαλία #999999 (.drew) · ήττα var(--err) (.lost)
Ράγα + δείκτες.fdacc-track.rail + ένα .ev ανά συμβάν, τοποθετημένο στο λεπτό/90 %
Άξονας.fdacc-ax0′90′
Chips συμβάντων.fdacc-evsένα pill ανά συμβάν: 9′ Ολυμπιακός
⚠️
Γκολ = κύκλοι, κάρτες = ορθογώνια. Γκολ δικό μας var(--win), αντιπάλου var(--err) — όπως στο 2b. Κάρτες: κίτρινη #FDB022, κόκκινη var(--err), με σχήμα .card. Η διαφορά σχήματος είναι δική μας προσθήκη: το 2b δεν είχε κάρτες στα δεδομένα του, και χωρίς αυτό μια κόκκινη κάρτα θα ήταν οπτικά ίδια με γκολ αντιπάλου. Επίσης κάθε δείκτης και chip έχει title/aria-label («Κίτρινη κάρτα 52′ — Ολυμπιακός»), γιατί το κείμενο του chip είναι 52′ Ολυμπιακός και για γκολ και για κάρτα.

3BΜπάσκετ — data-sport="basket" Q1–Q4 εκτός MVP

Ίδια bubbles, ίδια γεωμετρία, ίδιο μοτίβο αλληλεπίδρασης. Αλλάζει μόνο η λεπτομέρεια του αθλήματος. Το μπάσκετ δεν έχει ισοπαλία, οπότε εμφανίζονται μόνο .fw / .fl (Ν / Η) — ποτέ .fdr.

ΕΠΙΠΛΕΟΝ ΠΕΔΙΑ ΣΤΟ data-fm

ΠεδίοΤύποςΣημασία
qarray[[δικά,αντιπάλου], …] ανά περίοδο — 4, ή περισσότερα με παράταση
mvpstringπροαιρετικό, π.χ. "M. Lessort 17π 9ρ"

ΤΑ ΥΠΟΛΟΙΠΑ ΠΑΡΑΓΩΓΟΝΤΑΙ — ΜΗ ΤΑ ΣΤΕΛΝΕΙΣ

ΜέγεθοςΥπολογισμός
διαφορά περιόδουδικά − αντιπάλου
διαφορά αγώναgf − ga — εμφανίζεται στη γραμμή αποτελέσματος ως Νίκη +2
περίοδοι που κερδήθηκανπλήθος περιόδων με δικά > αντιπάλου
τελικό σκορτα gf/ga πρέπει να ισούνται με το άθροισμα του q

DESKTOP — ΧΩΡΙΣ ΑΝΑΛΥΣΗ ΠΕΡΙΟΔΩΝ

Το popover δείχνει έδρα · αντίπαλο, σκορ, αποτέλεσμα + διαφορά και τον κορυφαίο παίκτη — και τίποτα άλλο. Δεν έχει ανάλυση περιόδων: το .fpop-tl (0–90′ timeline του ποδοσφαίρου) απλώς δεν έχει αντίστοιχο στο μπάσκετ. Κρατά το κανονικό πλάτος 196px. Η ανά-περίοδο πληροφορία υπάρχει μόνο στο mobile modal.

MOBILE — ΑΝΑΛΥΣΗ ΠΕΡΙΟΔΩΝ ΣΤΟ MODAL

Το modal ανοίγει με όλες τις γραμμές κλειστές — μόνο η λίστα των 5 αγώνων. Πάτημα σε γραμμή της δίνει .is-open και εμφανίζει από κάτω .fdacc-res (τελεία + «Νίκη +2 · 2/4 περίοδοι · Εντός · παίκτης») και .fdacc-q με τέσσερα .fdq: chip διαφοράς, ζεύγος μπαρών, σκορ περιόδου, ετικέτα Q. Πάτημα σε άλλη γραμμή μεταφέρει την ανάλυση· πάτημα στην ίδια την κλείνει.

⚠️
Τα χρώματα είναι του design system, όχι του mockup. Το reference «Form Popup Mobile.html» (options 3a / 5a) χρησιμοποιεί μωβ #7A67E8, που δεν υπάρχει στο Foxbet DS. Εδώ: μπάρα ομάδας var(--acc) #CB2F01, μπάρα αντιπάλου var(--line), chip θετικής διαφοράς #D1FADF / var(--win), αρνητικής #FEE4E2 / var(--err).

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 που κόβει το popover — overflow hidden, auto ή scroll. Αν το popover δεν φαίνεται, αυτό λείπει.
  • Κάλεσε FormBubbles.init() μία φορά μετά το render. Ξανακάλεσέ το μετά από AJAX — είναι idempotent.
  • MVP: το κλικ ανά αγώνα μέσα στα mobile modals δεν είναι απαραίτητο. Αρκεί το modal + οι 5 αγώνες. Αν το κόψεις, μη στέλνεις e / q στο data-fm — μια γραμμή γίνεται αναπτύξιμη μόνο όταν έχει δεδομένα, οπότε παύει μόνη της να ανοίγει. Δεν χρειάζεται αλλαγή στο component.
  • Έλεγξε: υπογράμμιση στον τελευταίο παιγμένο αγώνα, και στις δύο περιπτώσεις (με και χωρίς ?).
91 αυτόματα tests στο tools/test_build.py καλύπτουν το contract, το scope των κλάσεων, τα tokens και τα contrast ratios. Τρέξε python -m unittest discover -s tools -p "test_*.py" από το foxbet-components/.
21ΕΕΕΠ | 21+ | ΠΑΙΞΕ ΥΠΕΥΘΥΝΑ