Η πρόσφατη φόρμα μιας ομάδας: κύκλοι που επικαλύπτονται, με το γράμμα του αποτελέσματος μέσα
(Ν / Ι / Η) και ? για τον επόμενο αγώνα. Τέσσερις παραλλαγές — desktop / mobile × ποδόσφαιρο / μπάσκετ.
Η γεωμετρία είναι κοινή· αλλάζει μόνο η συμπεριφορά και η λεπτομέρεια του αθλήματος.
Κάνε κλικ (και hover στα desktop) σε όλα.
Σκούρα επιφάνεια (hero banner). Hover, click ή focus ανοίγει το .fpop popover του
αγώνα: έδρα · αντίπαλος, σκορ, αποτέλεσμα + κάρτες, mini 0–90′ timeline.
Φωτεινή επιφάνεια (πίνακας βαθμολογίας). Tap σε οποιοδήποτε bubble ανοίγει το
.fdmodal με όλους τους αγώνες — όχι popover σε hover. Ανοίγει με όλες τις
γραμμές κλειστές· πάτα μια γραμμή και ανοίγει το timeline 0–90′ με γκολ και κάρτες
(ref 2b).
Ίδιο μοτίβο με το desktop football, αλλά αντί για timeline συμβάντων Το popover δείχνει σκορ και διαφορά ανά περίοδο δίπλα-δίπλα (ref 5a), όχι γράφημα. Το τρίτο bubble είναι ο αγώνας-καμπή ΠΑΟ 84-82 ΠΑΟΚ.
Ίδιο modal με το mobile football. Ανοίγει με όλες τις γραμμές κλειστές· πάτα μια γραμμή και ανοίγει σε ανάλυση Q1–Q4: διαφορά περιόδου, μπάρες σκορ, και «περίοδοι που κερδήθηκαν». Πάτα άλλη για να μεταφερθεί η ανάλυση.
Component form-bubbles v1.2.0 · ART-379 · βιβλιοθήκη
foxbet-components/components/form-bubbles/. Port από
foxbet-responsive.html. Όπου το κείμενο και το HTML διαφωνούν, ισχύει το HTML.
Αυτό το component δεν χρησιμοποιεί εικόνες. Τα bubbles είναι καθαρό CSS + ελληνικά γράμματα,
οπότε το assets στο component.json είναι σκόπιμα κενό: [].
ΤΟ ΜΟΝΟ ΓΡΑΦΙΚΟ — ΤΟ ΔΙΝΕΙ Ο CONSUMER
Το έμβλημα στην κεφαλίδα του modal δεν ανήκει στο component: το περνά η σελίδα
μέσω data-crest στο .formdots. Το component απλώς το βάζει σε
<img>. Αν λείπει, η κεφαλίδα δουλεύει κανονικά χωρίς εικόνα.
component.css + component.js και δουλεύει.Δύο ανεξάρτητοι άξονες στο .formdots: data-variant
(mobile / desktop) × data-sport
(football / basket, default football). Ο παρακάτω πίνακας
είναι ο άξονας data-variant· η διαφορά του αθλήματος είναι στο 3B.
| desktop | mobile | |
|---|---|---|
| Μοντέλο | bubbles του hero banner | bubbles της Βαθμολογίας |
| Επιφάνεια | σκούρα → υπογράμμιση #fff | φωτεινή → υπογράμμιση var(--ink) |
| Δείκτης | cursor:pointer + hover translateY(-3px) | :active{transform:scale(.85)} |
| Λεπτομέρεια | .fpop popover, ανά bubble | .fdmodal scrim + κάρτα, όλοι οι αγώνες |
| Ανοίγει με | click · hover ποντικιού · focus | tap |
| Κλείνει με | pointerleave · blur · pointerdown έξω · scroll | tap στο scrim · .fdmodal-x |
| Στοίχιση | flex default | justify-content:center |
data-variant="mobile" ανοίγει modal σε οποιοδήποτε πλάτος· το
data-variant="desktop" κρατά popover σε οποιοδήποτε πλάτος. Το
matchMedia('(max-width:1199.98px)') είναι μόνο fallback όταν δεν δηλώνεται
data-variant (legacy responsive σελίδες). Στο prototype μία responsive σελίδα άλλαζε
συμπεριφορά στο breakpoint· εδώ ο consumer δηλώνει τι θέλει.Η γεωμετρία είναι κοινή στις δύο παραλλαγές — γράφεται μία φορά.
| Στοιχείο | Τιμή | Σημείωση |
|---|---|---|
| Διάμετρος | 25 × 25px | + 2px λευκό border → 29px εξωτερικά |
| Επικάλυψη | margin-left:-8px | σε κάθε bubble εκτός του πρώτου· gap:0 |
| Radius | 50% | κύκλος |
| Γράμμα | Roboto Mono 12px / 700 | var(--num), λευκό |
| Υπογράμμιση | 2.5px, left/right 18%, bottom -5px | radius 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 βάζει την υπογράμμιση στο προηγούμενο.
Μη «απλοποιήσεις» αυτούς τους δύο κανόνες σε έναν.| Κατάσταση | Κλάση | Γράμμα | Fill |
|---|---|---|---|
| Νίκη | .fw | Ν | linear-gradient(135deg,#039855,var(--win)) |
| Ισοπαλία | .fdr | Ι | #999999 |
| Ήττα | .fl | Η | linear-gradient(135deg,var(--err),var(--errdark)) |
| Επόμενος | .fq | ? | var(--fill) + κείμενο #808080 |
Κανονική είσοδος: data-fm JSON σε κάθε παιγμένο bubble.
| Πεδίο | Τύπος | Σημασία |
|---|---|---|
v | "H" / "A" | έδρα — εντός / εκτός· παραλείπεται σε ουδέτερη έδρα |
o | string | αντίπαλος |
dt | string | ημερομηνία εμφάνισης, π.χ. "25 Μαρ" |
gf / ga | int | γκολ υπέρ / κατά (πάντα από τη σκοπιά της ομάδας) |
r | "W"/"D"/"L" | αποτέλεσμα |
y / rd | int | κίτρινες / κόκκινες κάρτες |
e | array | συμβάντα [[λεπτό, "g"|"y", "us"|"them"], …] για το timeline |
ΣΤΟ .formdots
| Attribute | Ρόλος |
|---|---|
data-variant | "mobile" ή "desktop" — υποχρεωτικό στη βιβλιοθήκη |
data-team | όνομα ομάδας για την κεφαλίδα του modal |
data-crest | URL εμβλήματος· προαιρετικό |
ΣΤΟ ? BUBBLE · ΚΑΙ LEGACY
| Attribute | Μορφή |
|---|---|
data-fmnext | "<ετικέτα>: vs <αντίπαλος> · <πότε>" — split στο πρώτο :, μετά στο ·· τελικό HH:MM = ώρα έναρξης |
data-tip | legacy — "Νίκη 3-0 vs Κόστα Ρίκα · 11 Ιουν". Το init() το μετατρέπει αυτόματα σε data-fm. Μη γράφεις νέο markup με αυτό. |
.standwrap → <tr> → .tm .full) για το όνομα και το
έμβλημα, και το modal γινόταν appendChild στο .phone. Στη βιβλιοθήκη αυτά
αντικαταστάθηκαν από data-team/data-crest και από
opts.mount. Η παλιά αναζήτηση προγόνου μένει μόνο ως fallback, ώστε οι
υπάρχουσες σελίδες να δουλεύουν χωρίς αλλαγή markup. Το hook για πίνακες με οριζόντιο scroll είναι
τώρα [data-fb-scroll], όχι το .standwrap.Ένα 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') });
| Κατάσταση | desktop | mobile |
|---|---|---|
| default | κύκλος 25px, γράμμα λευκό | |
| hover | translateY(-3px) + z-index:8 | — |
| focus | focus ring μέσω tabindex="0"· στο desktop ανοίγει το popover | |
| pressed | scale(.85) | |
| open | .fpop ανοιχτό | .fdmodal ανοιχτό |
| reduced motion | @media (prefers-reduced-motion:reduce) → transition:none | |
Το component δεν δηλώνει tokens — μόνο τα χρησιμοποιεί. Πηγή:
tokens/tokens.css.
| Token | Τιμή | Χρήση |
|---|---|---|
--ink | #1A1A1A | υπογράμμιση σε φωτεινή επιφάνεια, κείμενο |
--mut | #666666 | ημερομηνίες, δευτερεύον κείμενο |
--line | #E6E6E6 | borders, timeline |
--fill | #F2F2F2 | .fq fill, κεφαλίδα modal |
--acc | #CB2F01 | score 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..fd έχει tabindex="0" — προσβάσιμο από πληκτρολόγιο.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 σε #039855 | 3.73 | ✗ |
| Ισοπαλία | #FFFFFF σε #999999 | 2.85 | ✗ (και AA-large ✗) |
Επόμενος .fq | #808080 σε #F2F2F2 | 3.53 | ✗ |
| Νίκη — τέλος gradient | #FFFFFF σε #027A48 | 5.41 | ✓ |
| Ήττα — αρχή / τέλος | #FFFFFF σε #D92D20 / #912018 | 4.83 / 8.66 | ✓ |
contrastKnownFail του component.json, χωρίς αλλαγή χρωμάτων. Τα bubbles
δεν βασίζονται μόνο στο χρώμα — το γράμμα Ν/Ι/Η μεταφέρει το αποτέλεσμα, που καλύπτει το
SC 1.4.1 (Use of Colour). Το ανοιχτό θέμα είναι το contrast, όχι η κωδικοποίηση.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>
Το modal ανοίγει με όλες τις γραμμές κλειστές, και στα δύο αθλήματα. Πάτημα σε
γραμμή της δίνει .is-open και εμφανίζει από κάτω τη λεπτομέρεια· πάτημα σε άλλη τη
μεταφέρει, πάτημα στην ίδια την κλείνει. Μια γραμμή είναι αναπτύξιμη μόνο αν έχει δεδομένα:
q για μπάσκετ, e για ποδόσφαιρο.
ΠΟΔΟΣΦΑΙΡΟ — TIMELINE ΓΚΟΛ & ΚΑΡΤΩΝ ref 2b
| Στοιχείο | Κλάση | Περιεχόμενο |
|---|---|---|
| Γραμμή αποτελέσματος | .fdacc-res | τελεία + «Νίκη · Εκτός». Τελεία: νίκη var(--win) · ισοπαλία #999999 (.drew) · ήττα var(--err) (.lost) |
| Ράγα + δείκτες | .fdacc-track | .rail + ένα .ev ανά συμβάν, τοποθετημένο στο λεπτό/90 % |
| Άξονας | .fdacc-ax | 0′ … 90′ |
| Chips συμβάντων | .fdacc-evs | ένα pill ανά συμβάν: 9′ Ολυμπιακός |
var(--win), αντιπάλου var(--err) — όπως στο 2b. Κάρτες: κίτρινη
#FDB022, κόκκινη var(--err), με σχήμα .card. Η διαφορά
σχήματος είναι δική μας προσθήκη: το 2b δεν είχε κάρτες στα δεδομένα του, και χωρίς αυτό μια
κόκκινη κάρτα θα ήταν οπτικά ίδια με γκολ αντιπάλου. Επίσης κάθε δείκτης και chip έχει
title/aria-label («Κίτρινη κάρτα 52′ — Ολυμπιακός»), γιατί το κείμενο του
chip είναι 52′ Ολυμπιακός και για γκολ και για κάρτα.data-sport="basket"Ίδια bubbles, ίδια γεωμετρία, ίδιο μοτίβο αλληλεπίδρασης. Αλλάζει μόνο η
λεπτομέρεια του αθλήματος. Το μπάσκετ δεν έχει ισοπαλία, οπότε εμφανίζονται μόνο
.fw / .fl (Ν / Η) — ποτέ .fdr.
ΕΠΙΠΛΕΟΝ ΠΕΔΙΑ ΣΤΟ data-fm
| Πεδίο | Τύπος | Σημασία |
|---|---|---|
q | array | [[δικά,αντιπάλου], …] ανά περίοδο — 4, ή περισσότερα με παράταση |
mvp | string | προαιρετικό, π.χ. "M. Lessort 17π 9ρ" |
ΤΑ ΥΠΟΛΟΙΠΑ ΠΑΡΑΓΩΓΟΝΤΑΙ — ΜΗ ΤΑ ΣΤΕΛΝΕΙΣ
| Μέγεθος | Υπολογισμός |
|---|---|
| διαφορά περιόδου | δικά − αντιπάλου |
| διαφορά αγώνα | gf − ga — εμφανίζεται στη γραμμή αποτελέσματος ως Νίκη +2 |
| περίοδοι που κερδήθηκαν | πλήθος περιόδων με δικά > αντιπάλου |
| τελικό σκορ | τα gf/ga πρέπει να ισούνται με το άθροισμα του q |
DESKTOP — ΠΛΕΓΜΑ ΠΕΡΙΟΔΩΝ ΣΤΟ POPOVER ref 5a
Το .fpop-tl
(0–90′ timeline του ποδοσφαίρου) αντικαθίσταται από .fpop-qg: τέσσερις κολόνες, η
καθεμιά με ετικέτα περιόδου, σκορ περιόδου και διαφορά περιόδου — σκορ και διαφορά
δίπλα-δίπλα, χωρίς γράφημα. Το popover πλαταίνει στα 240px
(.fpop.is-basket), και το .fpop-mvp κλείνει με τον κορυφαίο παίκτη.
Παράδειγμα ΠΑΟ 84-82 ΠΑΟΚ: Q1 30-14 +16 · Q2 12-27 −15 ·
Q3 24-16 +8 · Q4 18-25 −7.
MOBILE — ΑΝΑΛΥΣΗ ΠΕΡΙΟΔΩΝ ΣΤΟ MODAL
Το modal ανοίγει με
όλες τις γραμμές κλειστές — μόνο η λίστα των 5 αγώνων. Πάτημα σε γραμμή της δίνει
.is-open και εμφανίζει από κάτω .fdacc-res (τελεία + «Νίκη +2 · 2/4
περίοδοι · Εντός · παίκτης») και .fdacc-q με τέσσερα .fdq: chip
διαφοράς, ζεύγος μπαρών, σκορ περιόδου, ετικέτα Q. Πάτημα σε άλλη γραμμή μεταφέρει την ανάλυση·
πάτημα στην ίδια την κλείνει.
#7A67E8, που
δεν υπάρχει στο Foxbet DS. Εδώ: μπάρα ομάδας var(--acc) #CB2F01, μπάρα
αντιπάλου var(--line), chip θετικής διαφοράς #D1FADF /
var(--win), αρνητικής #FEE4E2 / var(--err).| Εκτός | Λόγος |
|---|---|
.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 ονομάτων ισχύει, ώστε να μη χρειαστεί αλλαγή σε καμία σελίδα.component.css και component.js. Καμία εξάρτηση, κανένα asset.--win / --err / --errdark υπάρχουν στα tokens..formdots πρέπει να δηλώνει data-variant.data-team (και data-crest αν υπάρχει) σε κάθε ομάδα — αλλιώς η κεφαλίδα του modal πέφτει στο fallback.data-fm JSON. Μη γράφεις νέο data-tip.data-fb-scroll σε κάθε wrapper που κόβει το popover — overflow hidden, auto ή scroll. Αν το popover δεν φαίνεται, αυτό λείπει.FormBubbles.init() μία φορά μετά το render. Ξανακάλεσέ το μετά από AJAX — είναι idempotent.?).tools/test_build.py καλύπτουν το contract, το scope των κλάσεων, τα tokens και τα
contrast ratios. Τρέξε python -m unittest discover -s tools -p "test_*.py" από το
foxbet-components/.