Η πρόσφατη φόρμα μιας ομάδας: κύκλοι που επικαλύπτονται, με το γράμμα του αποτελέσματος μέσα
(Ν / Ι / Η) και ? για τον επόμενο αγώνα. Δύο παραλλαγές
— desktop (hero, popover ανά bubble) και mobile (Βαθμολογία, modal με όλους τους αγώνες).
Κάνε κλικ στα bubbles και στις δύο.
Σκούρα επιφάνεια (hero banner). Hover, click ή focus ανοίγει το .fpop popover του
συγκεκριμένου αγώνα: έδρα · αντίπαλος, σκορ, αποτέλεσμα + κάρτες, mini 0–90′ timeline.
Φωτεινή επιφάνεια (πίνακας βαθμολογίας). Tap σε οποιοδήποτε bubble ανοίγει το
.fdmodal με όλους τους αγώνες της ομάδας — όχι popover σε hover.
Component form-bubbles v1.0.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 και δουλεύει.Η παραλλαγή δηλώνεται με data-variant στο .formdots.
| 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>
ΠΙΝΑΚΑΣ ΜΕ ΟΡΙΖΟΝΤΙΟ SCROLL
<!-- βάλε data-fb-scroll στο wrapper: το popover γίνεται position:fixed --> <div class="standwrap" data-fb-scroll> <table>…</table> </div>
| Εκτός | Λόγος |
|---|---|
.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 πίνακα με οριζόντιο scroll.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/.