Files
segnapunti/src/components/ControllerPage.vue
T

1027 lines
30 KiB
Vue
Raw Normal View History

2026-02-10 00:42:48 +01:00
<template>
<section class="controller-page">
<!-- Barra di stato connessione -->
2026-02-10 00:42:48 +01:00
<div class="conn-bar" :class="{ connected: wsConnected }">
<span class="dot"></span>
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
</div>
<!-- MODALITÀ MOBILE -->
<template v-if="!isEstesa">
<!-- Anteprima punteggio -->
<div class="score-preview">
<div class="team-score" :class="primaSquadra + '-bg'" @click="sendAction({ type: 'incPunt', team: primaSquadra })">
<div class="team-name">{{ state.sp.nomi[primaSquadra] }}</div>
<div class="team-pts">{{ punt[primaSquadra] }}</div>
<div class="team-set">SET {{ set[primaSquadra] }}</div>
<img v-show="primaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
</div>
<div class="team-score" :class="secondaSquadra + '-bg'" @click="sendAction({ type: 'incPunt', team: secondaSquadra })">
<div class="team-name">{{ state.sp.nomi[secondaSquadra] }}</div>
<div class="team-pts">{{ punt[secondaSquadra] }}</div>
<div class="team-set">SET {{ set[secondaSquadra] }}</div>
<img v-show="secondaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="serv-icon" alt="Servizio" />
</div>
2026-02-10 00:42:48 +01:00
</div>
<!-- Riga annulla punto -->
<div class="undo-row">
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
ANNULLA PUNTO
</button>
2026-02-10 00:42:48 +01:00
</div>
<!-- Pulsanti set -->
<div class="action-row">
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
SET {{ state.sp.nomi.home }}
</button>
<button class="btn btn-set guest-bg" @click="sendAction({ type: 'incSet', team: 'guest' })">
SET {{ state.sp.nomi.guest }}
</button>
</div>
2026-02-10 00:42:48 +01:00
<!-- Controlli principali -->
<div class="controls">
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
Cambio Palla
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
{{ state.visuForm ? 'Punteggio' : 'Formazioni' }}
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}
</button>
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
Inverti
</button>
<button class="btn btn-ctrl" @click="speak()">
Voce
</button>
<button class="btn btn-ctrl" @click="openConfig()">
Config
</button>
<button class="btn btn-ctrl" @click="openCambiTeam()">
Cambi
</button>
<button class="btn btn-danger" @click="confirmReset = true">
Reset
</button>
</div>
</template>
<!-- MODALITÀ ESTESA -->
<div v-else class="e-dash" @wheel.prevent @touchmove.prevent>
<div class="e-panels">
<!-- Prima squadra -->
<div class="e-panel" :class="primaSquadra === 'home' ? 'e-panel--home' : 'e-panel--guest'">
<div class="e-panel__stripe"></div>
<div class="e-panel__body">
<header class="e-panel__head">
<span class="e-panel__name">{{ state.sp.nomi[primaSquadra] }}</span>
<span class="e-panel__meta">
<img v-show="primaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="e-panel__serv" />
{{ set[primaSquadra] }} <span class="e-label">SET</span>
</span>
</header>
<div class="e-panel__score" @click="sendAction({ type: 'incPunt', team: primaSquadra })">
{{ punt[primaSquadra] }}
</div>
<div class="e-panel__court">
<div class="e-court__row">
<div v-for="x in [3,2,1]" :key="'pf1'+x" class="e-court__cell">{{ state.sp.form[primaSquadra][x] }}</div>
</div>
<div class="e-court__net"></div>
<div class="e-court__row">
<div v-for="x in [4,5,0]" :key="'pf2'+x" class="e-court__cell">{{ state.sp.form[primaSquadra][x] }}</div>
</div>
</div>
<button class="btn e-panel__setbtn" @click="sendAction({ type: 'incSet', team: primaSquadra })">
SET {{ state.sp.nomi[primaSquadra] }}
</button>
</div>
</div>
<!-- Seconda squadra -->
<div class="e-panel" :class="secondaSquadra === 'home' ? 'e-panel--home' : 'e-panel--guest'">
<div class="e-panel__stripe"></div>
<div class="e-panel__body">
<header class="e-panel__head">
<span class="e-panel__name">{{ state.sp.nomi[secondaSquadra] }}</span>
<span class="e-panel__meta">
<img v-show="secondaSquadra === 'home' ? servHome : !servHome" src="/serv.png" class="e-panel__serv" />
{{ set[secondaSquadra] }} <span class="e-label">SET</span>
</span>
</header>
<div class="e-panel__score" @click="sendAction({ type: 'incPunt', team: secondaSquadra })">
{{ punt[secondaSquadra] }}
</div>
<div class="e-panel__court">
<div class="e-court__row">
<div v-for="x in [3,2,1]" :key="'sf1'+x" class="e-court__cell">{{ state.sp.form[secondaSquadra][x] }}</div>
</div>
<div class="e-court__net"></div>
<div class="e-court__row">
<div v-for="x in [4,5,0]" :key="'sf2'+x" class="e-court__cell">{{ state.sp.form[secondaSquadra][x] }}</div>
</div>
</div>
<button class="btn e-panel__setbtn" @click="sendAction({ type: 'incSet', team: secondaSquadra })">
SET {{ state.sp.nomi[secondaSquadra] }}
</button>
</div>
</div>
</div>
2026-02-10 00:42:48 +01:00
<!-- Barra azioni secondarie -->
<div class="e-actions">
<button class="btn e-act e-act--undo" @click="sendAction({ type: 'decPunt' })"> Annulla</button>
<button class="btn e-act" :disabled="!isPunteggioZeroZero" @click="sendAction({ type: 'cambiaPalla' })">Cambio Palla</button>
<button class="btn e-act" @click="openCambiTeam()">Cambi</button>
<button class="btn e-act" @click="sendAction({ type: 'toggleOrder' })">Inverti</button>
<button class="btn e-act" @click="sendAction({ type: 'toggleStriscia' })">Striscia {{ state.visuStriscia ? 'ON' : 'OFF' }}</button>
<button class="btn e-act" @click="speak()">Voce</button>
<button class="btn e-act" @click="openConfig()">Config</button>
<button class="btn e-act e-act--danger" @click="confirmReset = true">Reset</button>
</div>
2026-02-10 00:42:48 +01:00
</div>
<!-- DIALOGS (condivisi) -->
<!-- Finestra conferma reset -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
<div class="dialog">
<div class="dialog-title">Azzero punteggio?</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="confirmReset = false">NO</button>
<button class="btn btn-confirm" @click="doReset()">SI</button>
</div>
</div>
</div>
<!-- Finestra set vinto / partita finita -->
<div class="overlay" v-if="showSetVinto">
<div class="dialog">
<div class="dialog-title">{{ isPartitaFinita ? 'PARTITA FINITA' : 'SET VINTO' }}</div>
<div class="dialog-winner">{{ state.sp.nomi[setVintoTeam] }}</div>
<div class="dialog-subtitle" v-if="!isPartitaFinita">Configura le formazioni per il prossimo set</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="undoUltimoPoint()">INDIETRO</button>
<button v-if="isPartitaFinita" class="btn btn-confirm" @click="showSetVinto = false">CHIUDI</button>
<button v-else class="btn btn-confirm" @click="doNuovoSet()">VAI AL SET SUCCESSIVO</button>
</div>
</div>
</div>
<!-- Finestra configurazione -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
<div class="dialog dialog-config">
<div class="dialog-title">Configurazione</div>
<div class="form-group">
<label>Nome Home</label>
<input type="text" v-model="configData.nomeHome" class="input-field" />
</div>
<div class="form-group">
<label>Nome Guest</label>
<input type="text" v-model="configData.nomeGuest" class="input-field" />
</div>
<div class="form-group">
<label>Modalità partita</label>
<div class="mode-buttons">
<button :class="['btn', 'btn-mode', configData.modalita === '2/3' ? 'active' : '']"
@click="configData.modalita = '2/3'">2/3</button>
<button :class="['btn', 'btn-mode', configData.modalita === '3/5' ? 'active' : '']"
@click="configData.modalita = '3/5'">3/5</button>
<button :class="['btn', 'btn-mode', configData.modalita === 'amichevole' ? 'active' : '']"
@click="configData.modalita = 'amichevole'">Amichevole</button>
2026-02-10 00:42:48 +01:00
</div>
</div>
<div class="form-group">
<label>Formazione Home</label>
<div class="form-grid">
<div class="form-row">
<input type="text" v-model="configData.formHome[3]" class="input-num" />
<input type="text" v-model="configData.formHome[2]" class="input-num" />
<input type="text" v-model="configData.formHome[1]" class="input-num" />
</div>
<div class="form-line"></div>
<div class="form-row">
<input type="text" v-model="configData.formHome[4]" class="input-num" />
<input type="text" v-model="configData.formHome[5]" class="input-num" />
<input type="text" v-model="configData.formHome[0]" class="input-num" />
</div>
</div>
</div>
<div class="form-group">
<label>Formazione Guest</label>
<div class="form-grid">
<div class="form-row">
<input type="text" v-model="configData.formGuest[3]" class="input-num" />
<input type="text" v-model="configData.formGuest[2]" class="input-num" />
<input type="text" v-model="configData.formGuest[1]" class="input-num" />
</div>
<div class="form-line"></div>
<div class="form-row">
<input type="text" v-model="configData.formGuest[4]" class="input-num" />
<input type="text" v-model="configData.formGuest[5]" class="input-num" />
<input type="text" v-model="configData.formGuest[0]" class="input-num" />
</div>
</div>
</div>
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="showConfig = false">Annulla</button>
<button class="btn btn-confirm" @click="saveConfig()">OK</button>
</div>
</div>
</div>
<!-- Selezione squadra per cambi -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
<div class="dialog">
<div class="dialog-title">Scegli squadra</div>
<div class="dialog-buttons">
<button class="btn btn-set home-bg" @click="openCambi('home')">{{ state.sp.nomi.home }}</button>
<button class="btn btn-set guest-bg" @click="openCambi('guest')">{{ state.sp.nomi.guest }}</button>
</div>
</div>
</div>
<!-- Finestra gestione cambi -->
2026-02-10 00:42:48 +01:00
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
<div class="dialog">
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
<div class="cambi-container">
<div class="cambio-row" v-for="(c, i) in cambiData" :key="i">
<input type="text" v-model="c.in" placeholder="IN" class="input-num cambi-in-field" />
<span class="cambio-arrow"></span>
<input type="text" v-model="c.out" placeholder="OUT" class="input-num cambi-out-field" />
</div>
</div>
<div v-if="cambiError" class="cambi-error">{{ cambiError }}</div>
2026-02-10 00:42:48 +01:00
<div class="dialog-buttons">
<button class="btn btn-cancel" @click="closeCambi()">Annulla</button>
<button class="btn btn-confirm" :disabled="!cambiValid" @click="confermaCambi()">CONFERMA</button>
</div>
</div>
</div>
</section>
</template>
<script>
import { createWsMixin } from '../wsMixin.js'
2026-02-10 00:42:48 +01:00
export default {
name: "ControllerPage",
mixins: [createWsMixin('controller')],
2026-02-10 00:42:48 +01:00
data() {
return {
confirmReset: false,
showSetVinto: false,
setVintoTeam: null,
2026-02-10 00:42:48 +01:00
showConfig: false,
showCambiTeam: false,
showCambi: false,
cambiTeam: "home",
cambiData: [{ in: "", out: "" }, { in: "", out: "" }],
cambiError: "",
2026-02-10 00:42:48 +01:00
configData: {
nomeHome: "", nomeGuest: "", modalita: "3/5",
2026-02-10 00:42:48 +01:00
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
},
isLandscape: window.innerWidth > window.innerHeight,
2026-02-10 00:42:48 +01:00
}
},
computed: {
isEstesa() { return this.isLandscape },
primaSquadra() { return this.state.order ? 'home' : 'guest' },
secondaSquadra() { return this.state.order ? 'guest' : 'home' },
2026-02-10 00:42:48 +01:00
isPunteggioZeroZero() {
return this.state.sp.striscia.at(-1).ris === ''
2026-02-10 00:42:48 +01:00
},
squadraVincente() {
const { home, guest } = this.punt
const sv = this.set
const totSet = sv.home + sv.guest
const isSetDecisivo = this.state.modalitaPartita === '2/3' ? totSet >= 2 : totSet >= 4
const soglia = isSetDecisivo ? 15 : 25
if (home >= soglia && home - guest >= 2) return 'home'
if (guest >= soglia && guest - home >= 2) return 'guest'
return null
},
isPartitaFinita() {
if (!this.setVintoTeam || this.state.modalitaPartita === 'amichevole') return false
const setsToWin = this.state.modalitaPartita === '2/3' ? 2 : 3
return this.set[this.setVintoTeam] + 1 >= setsToWin
},
2026-02-10 00:42:48 +01:00
cambiValid() {
let hasComplete = false
for (const c of this.cambiData) {
const cin = (c.in || "").trim(), cout = (c.out || "").trim()
if (!cin && !cout) continue
if (!cin || !cout) return false
2026-02-10 00:42:48 +01:00
hasComplete = true
}
return hasComplete
},
2026-02-10 00:42:48 +01:00
},
watch: {
squadraVincente(val) {
if (val && !this.showSetVinto) {
this.setVintoTeam = val
this.showSetVinto = true
}
},
},
mounted() {
this._resizeHandler = () => { this.isLandscape = window.innerWidth > window.innerHeight }
window.addEventListener('resize', this._resizeHandler)
window.addEventListener('orientationchange', this._resizeHandler)
},
beforeUnmount() {
window.removeEventListener('resize', this._resizeHandler)
window.removeEventListener('orientationchange', this._resizeHandler)
},
2026-02-10 00:42:48 +01:00
methods: {
onWsMessage(msg) {
if (msg.type === 'error') this.showErrorFeedback(msg.message)
},
2026-02-10 00:42:48 +01:00
sendAction(action) {
if (!action?.type) return
if (!this.sendWs({ type: 'action', action })) this.showErrorFeedback('Non connesso al server')
2026-02-10 00:42:48 +01:00
},
showErrorFeedback(message) {
console.error('[Controller] Error:', message)
},
2026-02-10 00:42:48 +01:00
doReset() {
this.sendAction({ type: 'resetta' })
this.confirmReset = false
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
}
this.showConfig = true
2026-02-10 00:42:48 +01:00
},
undoUltimoPoint() {
this.sendAction({ type: 'decPunt' })
this.showSetVinto = false
},
doNuovoSet() {
this.sendAction({ type: 'nuovoSet', team: this.setVintoTeam })
this.showSetVinto = false
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: ["1", "2", "3", "4", "5", "6"],
formGuest: ["1", "2", "3", "4", "5", "6"],
}
this.showConfig = true
},
2026-02-10 00:42:48 +01:00
openConfig() {
this.configData = {
nomeHome: this.state.sp.nomi.home,
nomeGuest: this.state.sp.nomi.guest,
modalita: this.state.modalitaPartita,
formHome: [...this.state.sp.form.home],
formGuest: [...this.state.sp.form.guest],
}
2026-02-10 00:42:48 +01:00
this.showConfig = true
},
saveConfig() {
this.sendAction({ type: 'setNomi', home: this.configData.nomeHome, guest: this.configData.nomeGuest })
this.sendAction({ type: 'setModalita', modalita: this.configData.modalita })
this.sendAction({ type: 'setFormazione', team: 'home', form: this.configData.formHome })
this.sendAction({ type: 'setFormazione', team: 'guest', form: this.configData.formGuest })
this.showConfig = false
},
openCambiTeam() { this.showCambiTeam = true },
2026-02-10 00:42:48 +01:00
openCambi(team) {
this.showCambiTeam = false
this.cambiTeam = team
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
this.cambiError = ""
2026-02-10 00:42:48 +01:00
this.showCambi = true
},
closeCambi() {
this.showCambi = false
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
this.cambiError = ""
2026-02-10 00:42:48 +01:00
},
confermaCambi() {
if (!this.cambiValid) return
this.cambiError = ""
2026-02-10 00:42:48 +01:00
const cambi = this.cambiData
.filter(c => (c.in || "").trim() && (c.out || "").trim())
.map(c => ({ in: c.in.trim(), out: c.out.trim() }))
const formSimulata = this.state.sp.form[this.cambiTeam].map(v => String(v).trim())
for (const cambio of cambi) {
if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
this.cambiError = "I numeri dei giocatori devono essere cifre"; return
}
if (cambio.in === cambio.out) {
this.cambiError = `Il giocatore ${cambio.in} non può sostituire sé stesso`; return
}
if (formSimulata.includes(cambio.in)) {
this.cambiError = `Il giocatore ${cambio.in} è già in formazione`; return
}
if (!formSimulata.includes(cambio.out)) {
this.cambiError = `Il giocatore ${cambio.out} non è in formazione`; return
}
formSimulata.splice(formSimulata.indexOf(cambio.out), 1, cambio.in)
}
2026-02-10 00:42:48 +01:00
this.sendAction({ type: 'confermaCambi', team: this.cambiTeam, cambi })
this.closeCambi()
},
speak() {
const { home, guest } = this.punt
const text = home + guest === 0 ? "zero a zero"
: home === guest ? `${home} pari`
: this.servHome ? `${home} a ${guest}` : `${guest} a ${home}`
this.sendWs({ type: 'speak', text })
},
},
2026-02-10 00:42:48 +01:00
}
</script>
<style scoped>
.controller-page {
min-height: 100vh;
background: #111;
color: #fff;
padding: 8px;
padding-top: 36px;
font-family: 'Inter', system-ui, sans-serif;
-webkit-user-select: none;
user-select: none;
overflow-x: hidden;
2026-02-10 00:42:48 +01:00
}
/* Barra stato connessione */
2026-02-10 00:42:48 +01:00
.conn-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
background: #c62828;
color: white;
z-index: 200;
transition: background 0.3s;
}
.conn-bar.connected {
background: #2e7d32;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
}
/* ── MODALITÀ MOBILE ── */
2026-02-10 00:42:48 +01:00
.score-preview {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.team-score {
flex: 1;
border-radius: 16px;
padding: 16px 12px;
text-align: center;
cursor: pointer;
position: relative;
transition: transform 0.1s;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.team-score:active {
transform: scale(0.97);
}
.home-bg {
background: linear-gradient(145deg, #1a1a1a, #333);
border: 2px solid #fdd835;
color: #fdd835;
}
.guest-bg {
background: linear-gradient(145deg, #0d47a1, #1565c0);
border: 2px solid #64b5f6;
color: #fff;
}
.team-name {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
}
.team-pts {
font-size: 56px;
font-weight: 900;
line-height: 1;
}
.team-set {
font-size: 13px;
font-weight: 600;
opacity: 0.75;
margin-top: 4px;
}
.serv-icon {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
}
.undo-row {
margin-bottom: 8px;
}
.btn-undo {
width: 100%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: #ffab91;
padding: 10px;
font-size: 14px;
font-weight: 700;
border-radius: 12px;
cursor: pointer;
}
.btn-undo:active {
background: rgba(255,100,50,0.2);
}
.action-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.btn-set {
flex: 1;
padding: 12px;
font-size: 13px;
font-weight: 700;
border-radius: 12px;
cursor: pointer;
border: none;
text-transform: uppercase;
}
.btn-set:active {
transform: scale(0.97);
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.btn {
border: none;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.btn-ctrl {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #e0e0e0;
padding: 14px 8px;
font-size: 14px;
font-weight: 600;
border-radius: 12px;
transition: background 0.15s;
}
.btn-ctrl:active {
background: rgba(255,255,255,0.18);
}
.btn-ctrl:disabled {
opacity: 0.35;
}
2026-02-10 00:42:48 +01:00
.btn-danger {
background: rgba(198, 40, 40, 0.25);
border: 1px solid rgba(239, 83, 80, 0.4);
color: #ff8a80;
2026-02-10 00:42:48 +01:00
padding: 14px 8px;
font-size: 14px;
font-weight: 600;
border-radius: 12px;
}
.btn-danger:active {
background: rgba(198, 40, 40, 0.45);
}
/* ── MODALITÀ ESTESA ── */
.e-dash {
position: fixed;
top: 28px; /* altezza conn-bar */
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
padding: 10px;
gap: 8px;
box-sizing: border-box;
overflow: hidden;
background: #111;
}
.e-panels {
display: flex;
flex: 1;
gap: 10px;
min-height: 0;
}
/* Pannello squadra */
.e-panel {
flex: 1;
display: flex;
flex-direction: column;
border-radius: 16px;
overflow: hidden;
background: #161618;
border: 1px solid rgba(255,255,255,0.07);
min-height: 0;
}
/* Barra colorata in cima — identità squadra */
.e-panel__stripe {
height: 5px;
flex-shrink: 0;
}
.e-panel--home .e-panel__stripe { background: #f5c518; }
.e-panel--guest .e-panel__stripe { background: #2196f3; }
.e-panel__body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 14px 16px 14px;
min-height: 0;
}
/* Header */
.e-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.e-panel__name {
font-size: 15px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #f0f0f0;
}
.e-panel__meta {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 700;
}
.e-panel--home .e-panel__meta { color: #f5c518; }
.e-panel--guest .e-panel__meta { color: #2196f3; }
.e-label {
font-size: 9px;
letter-spacing: 1px;
opacity: 0.6;
font-weight: 600;
}
.e-panel__serv { width: 14px; height: 14px; }
/* Score — l'elemento eroe */
.e-panel__score {
font-size: clamp(60px, 14vh, 110px);
font-weight: 900;
line-height: 1;
text-align: center;
cursor: pointer;
border-radius: 12px;
padding: 6px 0;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
transition: opacity 0.1s, transform 0.1s;
user-select: none;
}
.e-panel__score:active { opacity: 0.65; transform: scale(0.96); }
.e-panel--home .e-panel__score { color: #f5c518; }
.e-panel--guest .e-panel__score { color: #2196f3; }
/* Campo formazione — diagramma del campo */
.e-panel__court {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0;
background: rgba(34, 80, 34, 0.18);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 8px 10px;
height: clamp(120px, 28vh, 200px);
}
.e-court__row {
flex: 1;
display: flex;
gap: 6px;
min-height: 0;
}
.e-court__cell {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(13px, 2.4vh, 22px);
font-weight: 700;
color: #ddd;
background: rgba(255,255,255,0.07);
border-radius: 6px;
min-height: 0;
}
.e-court__net {
height: 2px;
background: rgba(255,255,255,0.18);
margin: 5px 0;
border-radius: 1px;
flex-shrink: 0;
}
/* SET button — pieno colore, fondo pannello */
.e-panel__setbtn {
flex-shrink: 0;
width: 100%;
padding: 9px 0;
font-size: 11px;
font-weight: 800;
letter-spacing: 1.2px;
text-transform: uppercase;
border: none;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.1s;
}
.e-panel__setbtn:active { opacity: 0.75; transform: scale(0.98); }
.e-panel--home .e-panel__setbtn { background: #f5c518; color: #111; }
.e-panel--guest .e-panel__setbtn { background: #2196f3; color: #fff; }
/* Barra azioni secondarie */
.e-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
height: 40px;
}
.e-act {
flex: 1;
height: 100%;
font-family: inherit;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(255,255,255,0.07);
color: #bbb;
cursor: pointer;
white-space: nowrap;
-webkit-tap-highlight-color: transparent;
transition: background 0.12s;
}
.e-act:active { background: rgba(255,255,255,0.16); }
.e-act:disabled { opacity: 0.28; cursor: not-allowed; }
.e-act--undo { color: #ffab91; border-color: rgba(255,171,145,0.2); }
.e-act--danger { background: rgba(180,30,30,0.2); border-color: rgba(239,83,80,0.3); color: #ff6b6b; }
.e-act--danger:active { background: rgba(198,40,40,0.4); }
/* ── OVERLAY E DIALOGS ── */
2026-02-10 00:42:48 +01:00
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
padding: 16px;
}
.dialog {
background: #1e1e1e;
border-radius: 20px;
padding: 24px;
width: 100%;
max-width: 400px;
border: 1px solid rgba(255,255,255,0.12);
}
.dialog-winner {
font-size: 28px;
font-weight: 900;
text-align: center;
margin-bottom: 8px;
}
.dialog-subtitle {
font-size: 13px;
color: #aaa;
text-align: center;
margin-bottom: 4px;
}
2026-02-10 00:42:48 +01:00
.dialog-config {
max-height: 85vh;
overflow-y: auto;
}
.dialog-title {
font-size: 18px;
font-weight: 800;
text-align: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.12);
}
.dialog-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-cancel {
flex: 1;
background: rgba(255,255,255,0.08);
color: #aaa;
padding: 12px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
border: none;
cursor: pointer;
font-family: inherit;
2026-02-10 00:42:48 +01:00
}
.btn-confirm {
flex: 1;
background: #2e7d32;
color: white;
padding: 12px;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
border: none;
cursor: pointer;
font-family: inherit;
2026-02-10 00:42:48 +01:00
}
.btn-confirm:disabled {
opacity: 0.4;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #aaa;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.input-field {
width: 100%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 10px 14px;
border-radius: 10px;
font-size: 16px;
box-sizing: border-box;
}
.input-field:focus {
outline: none;
border-color: #64b5f6;
}
.input-num {
width: 52px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 8px;
border-radius: 8px;
font-size: 18px;
text-align: center;
box-sizing: border-box;
}
.input-num:focus {
outline: none;
border-color: #64b5f6;
}
.form-grid {
background: rgba(205, 133, 63, 0.15);
border: 2px solid rgba(255,255,255,0.15);
border-radius: 12px;
padding: 12px;
}
.form-row {
display: flex;
justify-content: center;
gap: 10px;
padding: 8px 0;
}
.form-line {
border-top: 1px dashed rgba(255,255,255,0.3);
margin: 4px 0;
}
.mode-buttons {
display: flex;
gap: 8px;
}
.btn-mode {
flex: 1;
padding: 10px;
background: rgba(255,255,255,0.08);
color: #aaa;
border-radius: 10px;
font-size: 16px;
font-weight: 700;
transition: all 0.15s;
border: none;
cursor: pointer;
font-family: inherit;
2026-02-10 00:42:48 +01:00
}
.btn-mode.active {
background: #2e7d32;
color: white;
}
.cambi-container {
display: flex;
flex-direction: column;
gap: 14px;
padding: 8px 0;
}
.cambio-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.cambio-arrow {
font-size: 20px;
font-weight: 700;
color: #aaa;
}
.cambi-in-field {
background: rgba(120, 200, 120, 0.2) !important;
border-color: rgba(120, 200, 120, 0.4) !important;
}
.cambi-out-field {
background: rgba(200, 120, 120, 0.2) !important;
border-color: rgba(200, 120, 120, 0.4) !important;
}
.cambi-error {
color: #ff6b6b;
font-size: 14px;
text-align: center;
padding: 8px 0;
font-weight: 600;
}
2026-02-10 00:42:48 +01:00
</style>