melissa/cronometro: pre-selecionar paciente + sessionPlan + confirm fechar
MelissaCronometro.abrir() agora aceita opts { pacienteId, autostart,
sessionPlan }. Retorna { opened, alreadyRunning, samePaciente, ... }
pra caller decidir o feedback. Estado sessionPlan { startH, endH }
exibe "Programado: HH:MM – HH:MM" sob o select + badge laranja
"atrasada Xmin" quando hNow > startH. Cronometro NAO auto-ajusta —
analista decide quando comecar/parar. Tick a 30s atualiza atraso.
sessionPlan persiste no localStorage junto com o snapshot.
X agora dispara confirmarFechar(): pede ConfirmDialog quando ha
sessao em andamento OU tempo decorrido nao salvo; fecha direto se
clean. Tooltip mudou pra "Encerrar sem salvar".
Chip minimizado: nome do paciente fica display:none em <md (mobile)
pra nao estourar largura do dock — icone + timer cobrem o essencial.
MelissaTimelineHoje: botao ⏱ overlay no canto sup. direito das pills
(horizontal + vertical) quando ev esta em curso E tem patient_id.
Pulso emerald sutil pra chamar atencao; @click.stop pra nao abrir
o evento. Novo emit iniciar-cronometro(ev).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
* Cronômetro de sessão estilo "janela do Windows":
|
* Cronômetro de sessão estilo "janela do Windows":
|
||||||
* - Dialog centralizado com select de paciente, display gigante e ações
|
* - Dialog centralizado com select de paciente, display gigante e ações
|
||||||
* - Click fora minimiza (chip no canto superior esquerdo)
|
* - Click fora minimiza (chip no canto superior esquerdo)
|
||||||
* - X/ESC fecha (destrói)
|
* - X = encerrar sem salvar. Com confirmacao se houver sessao em
|
||||||
|
* andamento ou tempo decorrido (fechar limpo nao pede confirm)
|
||||||
* - Botão inferior alterna entre "▶ Começar" e "⏹ Parar"
|
* - Botão inferior alterna entre "▶ Começar" e "⏹ Parar"
|
||||||
* - "+1 minuto" estende o tempo
|
* - "+1 minuto" estende o tempo
|
||||||
* - Quando minimizado, o timer continua rodando em background
|
* - Quando minimizado, o timer continua rodando em background
|
||||||
@@ -23,8 +24,11 @@
|
|||||||
* cronoRef.value.fechar() // destrói
|
* cronoRef.value.fechar() // destrói
|
||||||
*/
|
*/
|
||||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import { playToque } from './melissaToques';
|
import { playToque } from './melissaToques';
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const STORAGE_KEY = 'melissa.cronometro.v1';
|
const STORAGE_KEY = 'melissa.cronometro.v1';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -57,6 +61,16 @@ const seconds = ref(props.duracaoMinutos * 60);
|
|||||||
const pacienteId = ref(props.defaultPacienteId);
|
const pacienteId = ref(props.defaultPacienteId);
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
|
// Plano da sessao (horario programado original do evento na agenda).
|
||||||
|
// Setado quando o cronometro abre a partir de um evento da timeline —
|
||||||
|
// vide abrir({ sessionPlan: { startH, endH } }). Null quando aberto
|
||||||
|
// manualmente. Persiste em localStorage junto com o resto do snap.
|
||||||
|
const sessionPlan = ref(null); // { startH: number, endH: number } | null
|
||||||
|
// Tick a cada 30s pra recomputar atraso conforme o tempo passa (so
|
||||||
|
// quando cronometro existe; reseta no fechar).
|
||||||
|
const _planNowTick = ref(Date.now());
|
||||||
|
let _planNowTimer = null;
|
||||||
|
|
||||||
// True só durante a transição de minimizar (dialog → chip).
|
// True só durante a transição de minimizar (dialog → chip).
|
||||||
// Permite trocar a animação leave do dialog por "shrink + fly to dock"
|
// Permite trocar a animação leave do dialog por "shrink + fly to dock"
|
||||||
// (tipo macOS minimize) em vez do lift normal usado pra fechar.
|
// (tipo macOS minimize) em vez do lift normal usado pra fechar.
|
||||||
@@ -90,21 +104,86 @@ const pacienteNome = computed(() => {
|
|||||||
return p ? p.nome : '';
|
return p ? p.nome : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Formatador hh:mm a partir do startH decimal (ex: 11.5 → "11:30").
|
||||||
|
function _fmtHora(h) {
|
||||||
|
if (typeof h !== 'number' || Number.isNaN(h)) return '';
|
||||||
|
const hh = Math.floor(h);
|
||||||
|
const mm = Math.round((h - hh) * 60);
|
||||||
|
return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionPlanLabel = computed(() => {
|
||||||
|
const p = sessionPlan.value;
|
||||||
|
if (!p || typeof p.startH !== 'number' || typeof p.endH !== 'number') return '';
|
||||||
|
return `Programado: ${_fmtHora(p.startH)} – ${_fmtHora(p.endH)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atraso em minutos vs horario programado. Calcula em relacao ao
|
||||||
|
// _planNowTick (atualizado a cada 30s) pra nao recomputar a cada frame.
|
||||||
|
const atrasoMin = computed(() => {
|
||||||
|
const p = sessionPlan.value;
|
||||||
|
if (!p || typeof p.startH !== 'number') return 0;
|
||||||
|
// ref usada so pra forcar recompute periodico
|
||||||
|
void _planNowTick.value;
|
||||||
|
const d = new Date();
|
||||||
|
const hNow = d.getHours() + d.getMinutes() / 60;
|
||||||
|
const diff = hNow - p.startH;
|
||||||
|
if (diff <= 0) return 0;
|
||||||
|
return Math.round(diff * 60);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Watch: avisa parent quando dialog aparece/some ─────────────
|
// ── Watch: avisa parent quando dialog aparece/some ─────────────
|
||||||
watch(visible, (v) => emit('visible-change', v));
|
watch(visible, (v) => emit('visible-change', v));
|
||||||
|
|
||||||
// ── Ações ──────────────────────────────────────────────────────
|
// ── Ações ──────────────────────────────────────────────────────
|
||||||
function abrir() {
|
// abrir({ pacienteId, autostart }) — opts permitem pre-selecionar
|
||||||
|
// um paciente (ex: click no botao "iniciar sessao" da timeline) e
|
||||||
|
// auto-iniciar a contagem. Sem opts mantem comportamento legado.
|
||||||
|
// Retorna { opened, alreadyRunning, pacienteId } pra caller decidir
|
||||||
|
// (ex: mostrar toast se ja tem cronometro rodando de outro paciente).
|
||||||
|
function abrir(opts = {}) {
|
||||||
|
const requestedPid = Object.prototype.hasOwnProperty.call(opts, 'pacienteId')
|
||||||
|
? opts.pacienteId
|
||||||
|
: props.defaultPacienteId;
|
||||||
if (exists.value) {
|
if (exists.value) {
|
||||||
// Já existe — apenas restaura se tava minimizado (não cria outro)
|
// Ja existe — comportamento opcao (b): nao troca paciente. Apenas
|
||||||
|
// restaura visualmente se estava minimizado. Caller decide se
|
||||||
|
// mostra toast quando o paciente requisitado e' diferente do atual.
|
||||||
if (minimized.value) minimized.value = false;
|
if (minimized.value) minimized.value = false;
|
||||||
return;
|
return {
|
||||||
|
opened: false,
|
||||||
|
alreadyRunning: !!running.value,
|
||||||
|
pacienteId: pacienteId.value,
|
||||||
|
samePaciente: pacienteId.value === requestedPid
|
||||||
|
};
|
||||||
}
|
}
|
||||||
seconds.value = props.duracaoMinutos * 60;
|
seconds.value = props.duracaoMinutos * 60;
|
||||||
pacienteId.value = props.defaultPacienteId;
|
pacienteId.value = requestedPid;
|
||||||
running.value = false;
|
running.value = false;
|
||||||
minimized.value = false;
|
minimized.value = false;
|
||||||
exists.value = true;
|
exists.value = true;
|
||||||
|
// Plano programado (vem do evento da timeline) — usado pra exibir
|
||||||
|
// "Programado: HH:MM – HH:MM" e badge de atraso. Sanitiza pra
|
||||||
|
// garantir 2 numeros validos; senao limpa.
|
||||||
|
const plan = opts.sessionPlan;
|
||||||
|
if (plan && typeof plan.startH === 'number' && typeof plan.endH === 'number'
|
||||||
|
&& !Number.isNaN(plan.startH) && !Number.isNaN(plan.endH)) {
|
||||||
|
sessionPlan.value = { startH: plan.startH, endH: plan.endH };
|
||||||
|
} else {
|
||||||
|
sessionPlan.value = null;
|
||||||
|
}
|
||||||
|
if (opts.autostart) {
|
||||||
|
// Defer pra rodar dps do mount/render do dialog (toggle precisa
|
||||||
|
// do setInterval em proximo tick pra contar a partir do segundo
|
||||||
|
// cheio, nao perde a fracao do tick atual).
|
||||||
|
setTimeout(() => { if (exists.value && !running.value) toggle(); }, 0);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
opened: true,
|
||||||
|
alreadyRunning: false,
|
||||||
|
pacienteId: pacienteId.value,
|
||||||
|
samePaciente: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
@@ -161,9 +240,33 @@ function fechar() {
|
|||||||
running.value = false;
|
running.value = false;
|
||||||
minimized.value = false;
|
minimized.value = false;
|
||||||
exists.value = false;
|
exists.value = false;
|
||||||
|
sessionPlan.value = null; // limpa pra proxima abertura comecar zerada
|
||||||
emit('close');
|
emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fechar com confirmacao quando ha sessao em andamento ou tempo
|
||||||
|
// decorrido sem salvar. Estado "clean" (parado + nada decorrido)
|
||||||
|
// fecha direto pra nao atrapalhar quem abriu por engano.
|
||||||
|
function confirmarFechar() {
|
||||||
|
const totalInicial = props.duracaoMinutos * 60;
|
||||||
|
const temAtividade = running.value || seconds.value !== totalInicial;
|
||||||
|
if (!temAtividade) {
|
||||||
|
fechar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
confirm.require({
|
||||||
|
message: running.value
|
||||||
|
? 'Sessão em andamento — encerrar agora descarta o tempo cronometrado sem salvar no DB.'
|
||||||
|
: 'O cronômetro tem tempo decorrido que ainda não foi salvo. Quer descartar?',
|
||||||
|
header: 'Encerrar sessão sem salvar?',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptLabel: 'Encerrar sem salvar',
|
||||||
|
rejectLabel: 'Continuar sessão',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: () => fechar()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function ajustarMinutos(delta) {
|
function ajustarMinutos(delta) {
|
||||||
seconds.value += delta * 60;
|
seconds.value += delta * 60;
|
||||||
// Re-baselina savedAt pra restore após reload pegar o novo valor correto
|
// Re-baselina savedAt pra restore após reload pegar o novo valor correto
|
||||||
@@ -185,7 +288,8 @@ function saveState() {
|
|||||||
minimized: !!minimized.value,
|
minimized: !!minimized.value,
|
||||||
running: !!running.value,
|
running: !!running.value,
|
||||||
seconds: seconds.value,
|
seconds: seconds.value,
|
||||||
savedAt: Date.now()
|
savedAt: Date.now(),
|
||||||
|
sessionPlan: sessionPlan.value // null | { startH, endH }
|
||||||
};
|
};
|
||||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(snap)); } catch {}
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(snap)); } catch {}
|
||||||
}
|
}
|
||||||
@@ -223,6 +327,15 @@ function loadState() {
|
|||||||
seconds.value = restoredSeconds;
|
seconds.value = restoredSeconds;
|
||||||
exists.value = true;
|
exists.value = true;
|
||||||
running.value = false; // toggle abaixo flipa pra true
|
running.value = false; // toggle abaixo flipa pra true
|
||||||
|
// Restaura plano programado se foi serializado. Sanitiza shape:
|
||||||
|
// {startH:number, endH:number} valido ou null.
|
||||||
|
const sp = snap.sessionPlan;
|
||||||
|
if (sp && typeof sp.startH === 'number' && typeof sp.endH === 'number'
|
||||||
|
&& !Number.isNaN(sp.startH) && !Number.isNaN(sp.endH)) {
|
||||||
|
sessionPlan.value = { startH: sp.startH, endH: sp.endH };
|
||||||
|
} else {
|
||||||
|
sessionPlan.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (wasRunning) {
|
if (wasRunning) {
|
||||||
// Retoma o interval. NÃO toca o toque retroativo — se o tempo
|
// Retoma o interval. NÃO toca o toque retroativo — se o tempo
|
||||||
@@ -235,6 +348,19 @@ function loadState() {
|
|||||||
// Watch nas mudanças de estado discreto (não em seconds enquanto roda — savedAt+delta dá conta)
|
// Watch nas mudanças de estado discreto (não em seconds enquanto roda — savedAt+delta dá conta)
|
||||||
watch([exists, minimized, running, pacienteId], () => saveState());
|
watch([exists, minimized, running, pacienteId], () => saveState());
|
||||||
|
|
||||||
|
// Tick a cada 30s pra recomputar atraso. So roda quando o cronometro
|
||||||
|
// existe E tem plano programado — senao desperdicio.
|
||||||
|
watch([exists, sessionPlan], ([e, p]) => {
|
||||||
|
if (e && p) {
|
||||||
|
if (!_planNowTimer) {
|
||||||
|
_planNowTimer = setInterval(() => { _planNowTick.value = Date.now(); }, 30_000);
|
||||||
|
}
|
||||||
|
} else if (_planNowTimer) {
|
||||||
|
clearInterval(_planNowTimer);
|
||||||
|
_planNowTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Mount / Cleanup ────────────────────────────────────────────
|
// ── Mount / Cleanup ────────────────────────────────────────────
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadState();
|
loadState();
|
||||||
@@ -242,6 +368,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer) clearInterval(timer);
|
if (timer) clearInterval(timer);
|
||||||
|
if (_planNowTimer) clearInterval(_planNowTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
||||||
@@ -264,7 +391,7 @@ defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
|||||||
<button class="mc-glass-btn" title="Minimizar" @click="minimizar">
|
<button class="mc-glass-btn" title="Minimizar" @click="minimizar">
|
||||||
<i class="pi pi-window-minimize text-white/90 text-xs" />
|
<i class="pi pi-window-minimize text-white/90 text-xs" />
|
||||||
</button>
|
</button>
|
||||||
<button class="mc-glass-btn" title="Fechar" @click="fechar">
|
<button class="mc-glass-btn" title="Encerrar sem salvar" @click="confirmarFechar">
|
||||||
<i class="pi pi-times text-white/90 text-sm" />
|
<i class="pi pi-times text-white/90 text-sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,6 +411,17 @@ defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
|||||||
</select>
|
</select>
|
||||||
<i class="pi pi-chevron-down mc-select-icon" />
|
<i class="pi pi-chevron-down mc-select-icon" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Plano programado da sessao (so quando aberto via
|
||||||
|
evento da timeline). Mostra horario original +
|
||||||
|
badge de atraso se aplicavel — analista decide,
|
||||||
|
cronometro nao auto-ajusta. -->
|
||||||
|
<div v-if="sessionPlanLabel" class="mc-session-plan">
|
||||||
|
<i class="pi pi-calendar text-white/55 text-[0.7rem]" />
|
||||||
|
<span class="text-white/70 text-[0.78rem]">{{ sessionPlanLabel }}</span>
|
||||||
|
<span v-if="atrasoMin > 0" class="mc-session-plan__late">
|
||||||
|
atrasada {{ atrasoMin }} min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display gigante + steppers manuais (+5 / -5) -->
|
<!-- Display gigante + steppers manuais (+5 / -5) -->
|
||||||
@@ -436,6 +574,28 @@ defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plano programado — linha abaixo do select com horario original e
|
||||||
|
badge de atraso quando aplicavel. Tom secundario pra nao roubar
|
||||||
|
atencao do display gigante do cronometro. */
|
||||||
|
.mc-session-plan {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.mc-session-plan__late {
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(251, 146, 60, 0.18);
|
||||||
|
color: rgb(253, 186, 116);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid rgba(251, 146, 60, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Display gigante ──────────────────────────────────────── */
|
/* ─── Display gigante ──────────────────────────────────────── */
|
||||||
.mc-display {
|
.mc-display {
|
||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
@@ -567,6 +727,14 @@ defineExpose({ abrir, fechar, minimizar, restaurar, toggle, visible });
|
|||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
border-left: 1px solid var(--m-border-strong);
|
border-left: 1px solid var(--m-border-strong);
|
||||||
}
|
}
|
||||||
|
/* Em mobile (<md=768px) o chip vive num dock estreito que precisa
|
||||||
|
acomodar 4 builtins + ψ + tray. Esconde o nome do paciente — o
|
||||||
|
icone + timer ja sinalizam o estado, e o nome continua disponivel
|
||||||
|
ao restaurar o cronometro (click). */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mc-chip-name { display: none; }
|
||||||
|
.mc-chip { padding: 8px 12px; }
|
||||||
|
}
|
||||||
.mc-chip-pulse {
|
.mc-chip-pulse {
|
||||||
animation: mc-pulse 1.6s ease-in-out infinite;
|
animation: mc-pulse 1.6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const props = defineProps({
|
|||||||
filtroTipo: { type: String, default: null }
|
filtroTipo: { type: String, default: null }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['evento', 'clear-filter']);
|
const emit = defineEmits(['evento', 'clear-filter', 'iniciar-cronometro']);
|
||||||
|
|
||||||
|
// Helper exposto no template: mostra o botao ⏱ so quando o evento esta
|
||||||
|
// em curso E tem patient_id (atividade livre/bloqueio nao tem paciente).
|
||||||
|
function podeIniciarCrono(ev) {
|
||||||
|
return isEvEmCurso(ev) && !!ev?.patient_id;
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
// Range de horas (HORA_INICIO/HORA_FIM) — derivado de:
|
// Range de horas (HORA_INICIO/HORA_FIM) — derivado de:
|
||||||
@@ -382,6 +388,19 @@ onMounted(() => {
|
|||||||
:class="['tl-event-pill__status', statusIcon(ev)]"
|
:class="['tl-event-pill__status', statusIcon(ev)]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
<!-- Botao ⏱ overlay — so em sessoes em curso com
|
||||||
|
paciente. stopPropagation pra nao disparar
|
||||||
|
o click do pill que abre o evento. -->
|
||||||
|
<button
|
||||||
|
v-if="podeIniciarCrono(ev)"
|
||||||
|
type="button"
|
||||||
|
class="tl-event-pill__crono"
|
||||||
|
title="Iniciar cronômetro"
|
||||||
|
aria-label="Iniciar cronômetro"
|
||||||
|
@click.stop="emit('iniciar-cronometro', ev)"
|
||||||
|
>
|
||||||
|
<i class="pi pi-stopwatch" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 h-full flex flex-col items-center pointer-events-none z-20"
|
class="absolute top-0 h-full flex flex-col items-center pointer-events-none z-20"
|
||||||
@@ -459,6 +478,18 @@ onMounted(() => {
|
|||||||
{{ fmtHora(ev.startH) }} – {{ fmtHora(ev.endH) }}
|
{{ fmtHora(ev.startH) }} – {{ fmtHora(ev.endH) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="vt-event-label">{{ ev.label }}</div>
|
<div class="vt-event-label">{{ ev.label }}</div>
|
||||||
|
<!-- Botao ⏱ overlay — so em sessoes em curso com paciente.
|
||||||
|
stopPropagation pra nao disparar o click do pill. -->
|
||||||
|
<button
|
||||||
|
v-if="podeIniciarCrono(ev)"
|
||||||
|
type="button"
|
||||||
|
class="vt-event__crono"
|
||||||
|
title="Iniciar cronômetro"
|
||||||
|
aria-label="Iniciar cronômetro"
|
||||||
|
@click.stop="emit('iniciar-cronometro', ev)"
|
||||||
|
>
|
||||||
|
<i class="pi pi-stopwatch" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vt-now" :style="{ top: nowCursorTop }">
|
<div class="vt-now" :style="{ top: nowCursorTop }">
|
||||||
@@ -640,6 +671,46 @@ html:not(.app-dark) .tl-day-badge--feriado {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Botao ⏱ "Iniciar cronometro" — overlay no canto sup. direito do
|
||||||
|
pill em sessoes em curso. Cor solida pra destacar contra o bg
|
||||||
|
colorido do evento; pulso sutil pra chamar atencao sem irritar. */
|
||||||
|
.tl-event-pill__crono,
|
||||||
|
.vt-event__crono {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
|
border-radius: 9999px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: background-color 160ms ease, transform 160ms ease, border-color 160ms ease;
|
||||||
|
animation: tl-crono-pulse 2s ease-in-out infinite;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.tl-event-pill__crono:hover,
|
||||||
|
.vt-event__crono:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.85); /* emerald-500 — convida ao "play" */
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
transform: scale(1.08);
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
.tl-event-pill__crono > i,
|
||||||
|
.vt-event__crono > i {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
@keyframes tl-crono-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35), 0 0 0 0 rgba(16, 185, 129, 0.45); }
|
||||||
|
50% { box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35), 0 0 0 6px rgba(16, 185, 129, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Realizado: glow verde sutil (cor do bg ja eh verde) — celebra o feito */
|
/* Realizado: glow verde sutil (cor do bg ja eh verde) — celebra o feito */
|
||||||
.tl-pill--realizado {
|
.tl-pill--realizado {
|
||||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.28), 0 4px 12px rgba(16, 185, 129, 0.18);
|
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.28), 0 4px 12px rgba(16, 185, 129, 0.18);
|
||||||
|
|||||||
Reference in New Issue
Block a user