Separa app in client-server con WebSocket
- Aggiunto server Express + WebSocket (server.js) - Creata pagina Display (solo visualizzazione punteggio) - Creata pagina Controller (pannello comandi da mobile) - Aggiunto Vue Router con rotte / e /controller - Estratta logica di gioco condivisa in gameState.js
This commit is contained in:
704
src/components/ControllerPage.vue
Normal file
704
src/components/ControllerPage.vue
Normal file
@@ -0,0 +1,704 @@
|
||||
<template>
|
||||
<section class="controller-page">
|
||||
<!-- Connection status bar -->
|
||||
<div class="conn-bar" :class="{ connected: wsConnected }">
|
||||
<span class="dot"></span>
|
||||
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
|
||||
</div>
|
||||
|
||||
<!-- Score preview -->
|
||||
<div class="score-preview">
|
||||
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
||||
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.home }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.home }}</div>
|
||||
<img v-show="state.sp.servHome" src="/serv.png" class="serv-icon" />
|
||||
</div>
|
||||
<div class="team-score guest-bg" @click="sendAction({ type: 'incPunt', team: 'guest' })">
|
||||
<div class="team-name">{{ state.sp.nomi.guest }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.guest }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.guest }}</div>
|
||||
<img v-show="!state.sp.servHome" src="/serv.png" class="serv-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Undo row -->
|
||||
<div class="undo-row">
|
||||
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
|
||||
↩ ANNULLA PUNTO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Set buttons -->
|
||||
<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>
|
||||
|
||||
<!-- Controls -->
|
||||
<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
|
||||
</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>
|
||||
|
||||
<!-- Reset confirmation -->
|
||||
<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>
|
||||
|
||||
<!-- Config dialog -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Cambi team selection -->
|
||||
<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>
|
||||
|
||||
<!-- Cambi dialog -->
|
||||
<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 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>
|
||||
export default {
|
||||
name: "ControllerPage",
|
||||
data() {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
confirmReset: false,
|
||||
showConfig: false,
|
||||
showCambiTeam: false,
|
||||
showCambi: false,
|
||||
cambiTeam: "home",
|
||||
cambiData: [
|
||||
{ in: "", out: "" },
|
||||
{ in: "", out: "" },
|
||||
],
|
||||
configData: {
|
||||
nomeHome: "",
|
||||
nomeGuest: "",
|
||||
modalita: "3/5",
|
||||
formHome: ["1", "2", "3", "4", "5", "6"],
|
||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
state: {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPunteggioZeroZero() {
|
||||
return this.state.sp.punt.home === 0 && this.state.sp.punt.guest === 0
|
||||
},
|
||||
cambiValid() {
|
||||
let hasComplete = false
|
||||
let allValid = true
|
||||
this.cambiData.forEach((c) => {
|
||||
const cin = (c.in || "").trim()
|
||||
const cout = (c.out || "").trim()
|
||||
if (!cin && !cout) return
|
||||
if (!cin || !cout) { allValid = false; return }
|
||||
hasComplete = true
|
||||
})
|
||||
return allValid && hasComplete
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.ws) this.ws.close()
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${location.host}`
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.wsConnected = true
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('WS parse error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.wsConnected = false
|
||||
setTimeout(() => this.connectWebSocket(), 2000)
|
||||
}
|
||||
|
||||
this.ws.onerror = () => { this.wsConnected = false }
|
||||
},
|
||||
|
||||
sendAction(action) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||
}
|
||||
},
|
||||
|
||||
doReset() {
|
||||
this.sendAction({ type: 'resetta' })
|
||||
this.confirmReset = false
|
||||
},
|
||||
|
||||
openConfig() {
|
||||
this.configData.nomeHome = this.state.sp.nomi.home
|
||||
this.configData.nomeGuest = this.state.sp.nomi.guest
|
||||
this.configData.modalita = this.state.modalitaPartita
|
||||
this.configData.formHome = [...this.state.sp.form.home]
|
||||
this.configData.formGuest = [...this.state.sp.form.guest]
|
||||
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
|
||||
},
|
||||
|
||||
openCambi(team) {
|
||||
this.showCambiTeam = false
|
||||
this.cambiTeam = team
|
||||
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
|
||||
this.showCambi = true
|
||||
},
|
||||
|
||||
closeCambi() {
|
||||
this.showCambi = false
|
||||
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
|
||||
},
|
||||
|
||||
confermaCambi() {
|
||||
if (!this.cambiValid) return
|
||||
const cambi = this.cambiData
|
||||
.filter(c => (c.in || "").trim() && (c.out || "").trim())
|
||||
.map(c => ({ in: c.in.trim(), out: c.out.trim() }))
|
||||
this.sendAction({ type: 'confermaCambi', team: this.cambiTeam, cambi })
|
||||
this.closeCambi()
|
||||
},
|
||||
|
||||
speak() {
|
||||
const msg = new SpeechSynthesisUtterance()
|
||||
if (this.state.sp.punt.home + this.state.sp.punt.guest === 0) {
|
||||
msg.text = "zero a zero"
|
||||
} else if (this.state.sp.punt.home === this.state.sp.punt.guest) {
|
||||
msg.text = this.state.sp.punt.home + " pari"
|
||||
} else {
|
||||
if (this.state.sp.servHome) {
|
||||
msg.text = this.state.sp.punt.home + " a " + this.state.sp.punt.guest
|
||||
} else {
|
||||
msg.text = this.state.sp.punt.guest + " a " + this.state.sp.punt.home
|
||||
}
|
||||
}
|
||||
const voices = window.speechSynthesis.getVoices()
|
||||
msg.voice = voices.find(v => v.name === 'Google italiano')
|
||||
window.speechSynthesis.speak(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
/* Connection bar */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Score preview */
|
||||
.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 */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Set buttons */
|
||||
.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 grid */
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: rgba(198, 40, 40, 0.25);
|
||||
border: 1px solid rgba(239, 83, 80, 0.4);
|
||||
color: #ef5350;
|
||||
padding: 14px 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.btn-danger:active {
|
||||
background: rgba(198, 40, 40, 0.45);
|
||||
}
|
||||
|
||||
/* Overlays & Dialogs */
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
background: #2e7d32;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-confirm:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Form groups */
|
||||
.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 */
|
||||
.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 */
|
||||
.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;
|
||||
}
|
||||
.btn-mode.active {
|
||||
background: #2e7d32;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Cambi */
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user