Melissa: hub Configuracoes + Embed + 9 Pages novas + dialog blueprint dark
Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -27,6 +27,10 @@ import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vu
|
||||
import { useDeterminedCommitments } from '@/features/agenda/composables/useDeterminedCommitments';
|
||||
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
|
||||
import FirstResponseCard from '@/components/dashboard/FirstResponseCard.vue';
|
||||
// Timeline com paridade ao Melissa: respeita jornada (agenda_regras_semanais),
|
||||
// folga/feriado, scroll horizontal com min-slot + auto-scroll, eco lateral.
|
||||
import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings';
|
||||
import { useFeriados } from '@/composables/useFeriados';
|
||||
|
||||
const dashHeroSentinelRef = ref(null);
|
||||
const heroStuck = ref(false);
|
||||
@@ -644,15 +648,47 @@ const commitments = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const hoursRange = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
|
||||
const TL_START = 7,
|
||||
TL_END = 20,
|
||||
TL_SPAN = TL_END - TL_START;
|
||||
function toPercent(h, m) {
|
||||
return ((h + m / 60 - TL_START) / TL_SPAN) * 100;
|
||||
// ── Timeline: range derivado de agenda_regras_semanais (regra do dia
|
||||
// atual). Fallback: agenda_configuracoes global → 07–20h. Range expande
|
||||
// pra incluir eventos fora do expediente. Espelha o Melissa.
|
||||
const { settings: agendaSettings, workRules: agendaWorkRules, load: loadAgendaSettings } = useAgendaSettings();
|
||||
const { todos: feriadosList, load: loadFeriadosBase } = useFeriados();
|
||||
onMounted(() => {
|
||||
loadAgendaSettings();
|
||||
});
|
||||
|
||||
function _timeStrToHour(s, fb) {
|
||||
const str = String(s || '').slice(0, 5);
|
||||
const [h, m] = str.split(':').map(Number);
|
||||
if (Number.isFinite(h) && Number.isFinite(m)) return h + m / 60;
|
||||
return fb;
|
||||
}
|
||||
const todayRules = computed(() => {
|
||||
const dow = new Date().getDay();
|
||||
return (agendaWorkRules.value || []).filter((r) => r.dia_semana === dow && r.ativo !== false);
|
||||
});
|
||||
const isFolga = computed(() => todayRules.value.length === 0);
|
||||
const todayFeriado = computed(() => {
|
||||
const d = new Date();
|
||||
const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
return (feriadosList.value || []).find((f) => f.data === k) || null;
|
||||
});
|
||||
function _baseTimelineRange() {
|
||||
const rules = todayRules.value;
|
||||
if (rules.length > 0) {
|
||||
const starts = rules.map((r) => _timeStrToHour(r.hora_inicio, 7));
|
||||
const ends = rules.map((r) => _timeStrToHour(r.hora_fim, 20));
|
||||
return { start: Math.min(...starts), end: Math.max(...ends) };
|
||||
}
|
||||
const s = agendaSettings.value;
|
||||
const fbStart = (s?.usar_horario_admin_custom && s?.admin_inicio_visualizacao) || s?.agenda_custom_start || '07:00';
|
||||
const fbEnd = (s?.usar_horario_admin_custom && s?.admin_fim_visualizacao) || s?.agenda_custom_end || '20:00';
|
||||
return { start: _timeStrToHour(fbStart, 7), end: _timeStrToHour(fbEnd, 20) };
|
||||
}
|
||||
|
||||
const timelineEvents = computed(() =>
|
||||
// `timelineEventsRaw` extrai a info dos eventos SEM depender de TL_START/TL_END
|
||||
// (pra evitar dep circular: TL_START depende dos eventos pra expandir o range).
|
||||
const timelineEventsRaw = computed(() =>
|
||||
_statsDoMes.value.timelineLista
|
||||
.slice()
|
||||
.sort((a, b) => new Date(a.inicio_em) - new Date(b.inicio_em))
|
||||
@@ -660,25 +696,178 @@ const timelineEvents = computed(() =>
|
||||
const item = buildEventoItem(ev);
|
||||
const [hh, mm] = item.hora.split(':').map(Number);
|
||||
const durMin = parseInt(item.dur) || 50;
|
||||
const startH = hh + mm / 60;
|
||||
const endH = startH + durMin / 60;
|
||||
return {
|
||||
id: item.id,
|
||||
inicio_em: ev.inicio_em,
|
||||
label: item.nome.split(' ')[0],
|
||||
tipo: item.tipo,
|
||||
status: item.status,
|
||||
modalidade: item.modalidade,
|
||||
tooltip: `${item.hora} · ${item.nome} · ${item.modalidade}`,
|
||||
badge: item.modalidade?.toLowerCase() === 'online' ? '📱' : '',
|
||||
bgColor: item.bgColor,
|
||||
txtColor: item.txtColor,
|
||||
style: { left: toPercent(hh, mm) + '%', width: Math.max((durMin / 60 / TL_SPAN) * 100, 4) + '%' }
|
||||
durMin,
|
||||
startH,
|
||||
endH
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const TL_START = computed(() => {
|
||||
const { start } = _baseTimelineRange();
|
||||
const evs = timelineEventsRaw.value || [];
|
||||
const minEv = evs.length ? Math.min(...evs.map((e) => e.startH)) : Infinity;
|
||||
return Math.max(0, Math.floor(Math.min(start, minEv)));
|
||||
});
|
||||
const TL_END = computed(() => {
|
||||
const { end } = _baseTimelineRange();
|
||||
const evs = timelineEventsRaw.value || [];
|
||||
const maxEv = evs.length ? Math.max(...evs.map((e) => e.endH)) : -Infinity;
|
||||
return Math.min(24, Math.ceil(Math.max(end, maxEv)));
|
||||
});
|
||||
const hoursRange = computed(() => {
|
||||
const arr = [];
|
||||
for (let h = TL_START.value; h <= TL_END.value; h++) arr.push(h);
|
||||
return arr;
|
||||
});
|
||||
function toPercent(h, m) {
|
||||
const span = TL_END.value - TL_START.value;
|
||||
if (span <= 0) return 0;
|
||||
return ((h + m / 60 - TL_START.value) / span) * 100;
|
||||
}
|
||||
|
||||
// `timelineEvents` adiciona positioning (left/width %) baseado em TL_START/END.
|
||||
const timelineEvents = computed(() =>
|
||||
timelineEventsRaw.value.map((ev) => {
|
||||
const span = TL_END.value - TL_START.value;
|
||||
const left = span > 0 ? ((ev.startH - TL_START.value) / span) * 100 : 0;
|
||||
const width = span > 0 ? Math.max(((ev.endH - ev.startH) / span) * 100, 4) : 4;
|
||||
return { ...ev, style: { left: `${left}%`, width: `${width}%` } };
|
||||
})
|
||||
);
|
||||
|
||||
const nowCursorLeft = computed(() => {
|
||||
const pct = toPercent(agora.value.getHours(), agora.value.getMinutes());
|
||||
return Math.min(Math.max(pct, 0), 100) + '%';
|
||||
});
|
||||
|
||||
// ── Scroll horizontal + eco lateral (paridade Melissa) ──────────
|
||||
// scroll state, eco state, auto-scroll to-now, scrollToEvent.
|
||||
const tlHScrollEl = ref(null);
|
||||
const tlScrollState = ref({ scrollL: 0, viewW: 0, innerW: 0 });
|
||||
function _updateTlScrollState() {
|
||||
const el = tlHScrollEl.value;
|
||||
if (!el) {
|
||||
tlScrollState.value = { scrollL: 0, viewW: 0, innerW: 0 };
|
||||
return;
|
||||
}
|
||||
const inner = el.firstElementChild;
|
||||
tlScrollState.value = {
|
||||
scrollL: el.scrollLeft,
|
||||
viewW: el.clientWidth,
|
||||
innerW: inner ? (inner.scrollWidth || inner.offsetWidth) : 0
|
||||
};
|
||||
}
|
||||
function onTimelineScroll() {
|
||||
_updateTlScrollState();
|
||||
}
|
||||
const tlEcoState = computed(() => {
|
||||
const { scrollL, viewW, innerW } = tlScrollState.value;
|
||||
const total = TL_END.value - TL_START.value;
|
||||
const empty = { left: [], right: [], vStart: TL_START.value, vEnd: TL_END.value };
|
||||
if (total <= 0 || !innerW || !viewW || innerW <= viewW) return empty;
|
||||
const vStart = TL_START.value + (scrollL / innerW) * total;
|
||||
const vEnd = TL_START.value + ((scrollL + viewW) / innerW) * total;
|
||||
const left = [];
|
||||
const right = [];
|
||||
for (const ev of timelineEventsRaw.value) {
|
||||
if (ev.endH <= vStart) left.push(ev);
|
||||
else if (ev.startH >= vEnd) right.push(ev);
|
||||
}
|
||||
return { left, right, vStart, vEnd };
|
||||
});
|
||||
function ecoTickStyle(ev, side) {
|
||||
const { vStart, vEnd } = tlEcoState.value;
|
||||
let topPct = 50;
|
||||
if (side === 'left') {
|
||||
const span = vStart - TL_START.value;
|
||||
if (span > 0) topPct = ((ev.startH - TL_START.value) / span) * 100;
|
||||
} else {
|
||||
const span = TL_END.value - vEnd;
|
||||
if (span > 0) topPct = ((ev.startH - vEnd) / span) * 100;
|
||||
}
|
||||
return {
|
||||
top: `${Math.max(0, Math.min(100, topPct))}%`,
|
||||
backgroundColor: ev.bgColor || '#6366f1'
|
||||
};
|
||||
}
|
||||
function scrollToEvent(ev) {
|
||||
const el = tlHScrollEl.value;
|
||||
if (!el) return;
|
||||
const inner = el.firstElementChild;
|
||||
if (!inner) return;
|
||||
const innerWidth = inner.scrollWidth || inner.offsetWidth;
|
||||
const visibleWidth = el.clientWidth;
|
||||
const total = TL_END.value - TL_START.value;
|
||||
if (total <= 0) return;
|
||||
const ratio = (ev.startH - TL_START.value) / total;
|
||||
const target = Math.max(0, Math.min(innerWidth - visibleWidth, ratio * innerWidth - visibleWidth / 2));
|
||||
el.scrollTo({ left: target, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
let _tlAutoScrolled = false;
|
||||
function _scrollTimelineToNow() {
|
||||
const el = tlHScrollEl.value;
|
||||
if (!el) return;
|
||||
const h = agora.value.getHours() + agora.value.getMinutes() / 60;
|
||||
const total = TL_END.value - TL_START.value;
|
||||
if (total <= 0 || h < TL_START.value || h > TL_END.value) return;
|
||||
const inner = el.firstElementChild;
|
||||
if (!inner) return;
|
||||
const innerWidth = inner.scrollWidth || inner.offsetWidth;
|
||||
const visibleWidth = el.clientWidth;
|
||||
if (innerWidth <= visibleWidth) return;
|
||||
const ratio = (h - TL_START.value) / total;
|
||||
el.scrollLeft = Math.max(0, ratio * innerWidth - visibleWidth / 2);
|
||||
}
|
||||
onMounted(() => {
|
||||
const stop = watch(
|
||||
[TL_START, TL_END],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (!_tlAutoScrolled) {
|
||||
_scrollTimelineToNow();
|
||||
const el = tlHScrollEl.value;
|
||||
const inner = el?.firstElementChild;
|
||||
if (inner && inner.scrollWidth > el.clientWidth) {
|
||||
_tlAutoScrolled = true;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
_updateTlScrollState();
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const el = tlHScrollEl.value;
|
||||
if (el && typeof ResizeObserver !== 'undefined') {
|
||||
const ro = new ResizeObserver(() => _updateTlScrollState());
|
||||
ro.observe(el);
|
||||
if (el.firstElementChild) ro.observe(el.firstElementChild);
|
||||
onBeforeUnmount(() => ro.disconnect());
|
||||
}
|
||||
});
|
||||
|
||||
function _fmtHora(h) {
|
||||
const horas = Math.floor(h);
|
||||
const mins = Math.round((h - horas) * 60);
|
||||
return `${String(horas).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
@@ -689,6 +878,10 @@ async function load() {
|
||||
}
|
||||
await tenantStore.ensureLoaded();
|
||||
const tid = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||||
// Feriados pra badge da timeline (precisa do tenant — RLS).
|
||||
if (tid) {
|
||||
loadFeriadosBase(tid).catch(() => {});
|
||||
}
|
||||
await loadCommitments();
|
||||
const mesInicio = new Date(anoAtual, mesAtual, 1, 0, 0, 0, 0).toISOString();
|
||||
const mesFim = new Date(anoAtual, mesAtual + 1, 0, 23, 59, 59, 999).toISOString();
|
||||
@@ -987,7 +1180,25 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col leading-tight">
|
||||
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Linha do tempo — Hoje</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Linha do tempo — Hoje</div>
|
||||
<span
|
||||
v-if="todayFeriado"
|
||||
class="dash-tl-badge dash-tl-badge--feriado"
|
||||
:title="`Feriado: ${todayFeriado.nome}`"
|
||||
>
|
||||
<i class="pi pi-star text-[0.6rem]" />
|
||||
Feriado{{ todayFeriado.nome ? `: ${todayFeriado.nome}` : '' }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="isFolga"
|
||||
class="dash-tl-badge dash-tl-badge--folga"
|
||||
title="Hoje não é dia de trabalho na sua agenda — sessões fora do expediente continuam permitidas."
|
||||
>
|
||||
<i class="pi pi-moon text-[0.6rem]" />
|
||||
Folga
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs font-semibold text-[var(--text-color-secondary)] flex items-center gap-1.5">
|
||||
<span class="pulse-dot w-[15px] h-[5px] rounded-full bg-red-500"></span>
|
||||
Agora: {{ horaAtual }}
|
||||
@@ -995,37 +1206,74 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exemplo: badge ou ação -->
|
||||
<span class="text-xs font-semibold text-[var(--text-color-secondary)]">
|
||||
<Button icon="pi pi-cog" severity="secondary" outlined class="rounded-full" title="Ver sua Agenda" @click="$router.push('/therapist/agenda')" label="Agenda" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2.5 relative">
|
||||
<div class="flex justify-between mb-1">
|
||||
<div v-for="h in hoursRange" :key="h" class="flex-1 text-left">
|
||||
<span class="text-[0.80rem] text-[var(--text-color-secondary)] font-semibold">{{ h }}h</span>
|
||||
<!-- Frame relativo abriga scroll horizontal + eco lateral overlay -->
|
||||
<div class="dash-tl-frame mt-2.5 relative">
|
||||
<div ref="tlHScrollEl" class="dash-tl-scroll" @scroll.passive="onTimelineScroll">
|
||||
<div class="dash-tl-inner relative" :style="{ '--m-tl-cols': TL_END - TL_START }">
|
||||
<div class="flex justify-between mb-1">
|
||||
<div v-for="h in hoursRange" :key="h" class="flex-1 text-left">
|
||||
<span class="text-[0.80rem] text-[var(--text-color-secondary)] font-semibold">{{ h }}h</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-10 bg-[var(--surface-ground,#f8fafc)] rounded-md overflow-visible">
|
||||
<div
|
||||
v-for="ev in timelineEvents"
|
||||
:key="ev.id"
|
||||
class="absolute top-[3px] h-[34px] rounded flex items-center px-1.5 overflow-hidden cursor-default min-w-[32px] hover:brightness-110 transition-[filter] duration-150 z-10"
|
||||
:style="{ ...ev.style, ...(ev.bgColor ? { backgroundColor: ev.bgColor, color: ev.txtColor || '#fff' } : {}) }"
|
||||
:class="{
|
||||
'bg-sky-400': !ev.bgColor && ev.tipo === 'reuniao',
|
||||
'bg-green-500': !ev.bgColor && ev.status === 'realizado',
|
||||
'bg-[var(--primary-color,#6366f1)]': !ev.bgColor && ev.tipo !== 'reuniao' && ev.status !== 'realizado'
|
||||
}"
|
||||
:title="ev.tooltip"
|
||||
>
|
||||
<span class="text-[0.58rem] font-bold text-white truncate">{{ ev.label }}</span>
|
||||
<span v-if="ev.badge" class="text-xs ml-auto">{{ ev.badge }}</span>
|
||||
</div>
|
||||
<div class="absolute top-0 h-full flex flex-col items-center pointer-events-none z-20" :style="{ left: nowCursorLeft }">
|
||||
<div class="w-0.5 h-full bg-red-500 opacity-80" />
|
||||
<div class="absolute -top-0.5 w-[7px] h-[7px] rounded-full bg-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-10 bg-[var(--surface-ground,#f8fafc)] rounded-md overflow-visible">
|
||||
<div
|
||||
v-for="ev in timelineEvents"
|
||||
:key="ev.id"
|
||||
class="absolute top-[3px] h-[34px] rounded flex items-center px-1.5 overflow-hidden cursor-default min-w-[32px] hover:brightness-110 transition-[filter] duration-150 z-10"
|
||||
:style="{ ...ev.style, ...(ev.bgColor ? { backgroundColor: ev.bgColor, color: ev.txtColor || '#fff' } : {}) }"
|
||||
:class="{
|
||||
'bg-sky-400': !ev.bgColor && ev.tipo === 'reuniao',
|
||||
'bg-green-500': !ev.bgColor && ev.status === 'realizado',
|
||||
'bg-[var(--primary-color,#6366f1)]': !ev.bgColor && ev.tipo !== 'reuniao' && ev.status !== 'realizado'
|
||||
}"
|
||||
:title="ev.tooltip"
|
||||
>
|
||||
<span class="text-[0.58rem] font-bold text-white truncate">{{ ev.label }}</span>
|
||||
<span v-if="ev.badge" class="text-xs ml-auto">{{ ev.badge }}</span>
|
||||
</div>
|
||||
<div class="absolute top-0 h-full flex flex-col items-center pointer-events-none z-20" :style="{ left: nowCursorLeft }">
|
||||
<div class="w-0.5 h-full bg-red-500 opacity-80" />
|
||||
<div class="absolute -top-0.5 w-[7px] h-[7px] rounded-full bg-red-500" />
|
||||
</div>
|
||||
|
||||
<!-- Eco lateral — minimap pulsante de cores. Tracinhos
|
||||
posicionados por tempo, click suaviza scroll até o evento. -->
|
||||
<div
|
||||
v-if="tlEcoState.left.length"
|
||||
class="dash-tl-eco dash-tl-eco--left"
|
||||
:title="`${tlEcoState.left.length} antes — clique pra centralizar`"
|
||||
>
|
||||
<button
|
||||
v-for="ev in tlEcoState.left"
|
||||
:key="`eco-l-${ev.id}`"
|
||||
type="button"
|
||||
class="dash-tl-eco__tick"
|
||||
:style="ecoTickStyle(ev, 'left')"
|
||||
:title="`${_fmtHora(ev.startH)} · ${ev.label}`"
|
||||
@click="scrollToEvent(ev)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="tlEcoState.right.length"
|
||||
class="dash-tl-eco dash-tl-eco--right"
|
||||
:title="`${tlEcoState.right.length} à frente — clique pra centralizar`"
|
||||
>
|
||||
<button
|
||||
v-for="ev in tlEcoState.right"
|
||||
:key="`eco-r-${ev.id}`"
|
||||
type="button"
|
||||
class="dash-tl-eco__tick"
|
||||
:style="ecoTickStyle(ev, 'right')"
|
||||
:title="`${_fmtHora(ev.startH)} · ${ev.label}`"
|
||||
@click="scrollToEvent(ev)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1423,4 +1671,103 @@ onMounted(async () => {
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
/* ─── Timeline horizontal — paridade com o Melissa ──────────────
|
||||
Range derivado de agenda_regras_semanais + scroll horizontal
|
||||
com min-width de slot pra legibilidade + eco lateral pra eventos
|
||||
off-screen. CSS escopado ao Dashboard (mesmas classes existem
|
||||
no MelissaLayout sob outros nomes — duplicação consciente até
|
||||
eventual extração pra componente compartilhado). */
|
||||
.dash-tl-frame { position: relative; }
|
||||
|
||||
.dash-tl-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
padding-bottom: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--surface-300, #cbd5e1) transparent;
|
||||
}
|
||||
.dash-tl-scroll::-webkit-scrollbar { height: 6px; }
|
||||
.dash-tl-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.dash-tl-scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-300, #cbd5e1);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.dash-tl-inner {
|
||||
/* --m-tl-cols inline = TL_END - TL_START. Default 13 cobre 7→20. */
|
||||
min-width: calc(var(--m-tl-cols, 13) * 80px);
|
||||
}
|
||||
|
||||
/* Badges (Folga / Feriado) — light theme nativo */
|
||||
.dash-tl-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-tl-badge--folga {
|
||||
background: var(--surface-100, #f1f5f9);
|
||||
color: var(--text-color-secondary, #64748b);
|
||||
border: 1px solid var(--surface-300, #cbd5e1);
|
||||
}
|
||||
.dash-tl-badge--feriado {
|
||||
background: color-mix(in srgb, rgb(217, 119, 6) 12%, transparent);
|
||||
color: rgb(180, 83, 9);
|
||||
border: 1px solid color-mix(in srgb, rgb(217, 119, 6) 32%, transparent);
|
||||
}
|
||||
|
||||
/* Eco lateral — minimap pulsante */
|
||||
.dash-tl-eco {
|
||||
position: absolute;
|
||||
top: 24px; /* alinha com a barra (descontando linha de horas) */
|
||||
bottom: 8px;
|
||||
width: 8px;
|
||||
z-index: 6;
|
||||
pointer-events: auto;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--surface-200, #e2e8f0) 70%, transparent);
|
||||
border: 1px solid var(--surface-300, #cbd5e1);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
animation: dash-tl-eco-pulse 2400ms ease-in-out infinite;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.dash-tl-eco--left { left: -2px; }
|
||||
.dash-tl-eco--right { right: -2px; }
|
||||
|
||||
.dash-tl-eco__tick {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
height: 4px;
|
||||
transform: translateY(-50%);
|
||||
border: 0;
|
||||
padding: 0;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
transition: opacity 140ms ease, transform 140ms ease, height 140ms ease;
|
||||
}
|
||||
.dash-tl-eco__tick:hover {
|
||||
opacity: 1;
|
||||
height: 6px;
|
||||
transform: translateY(-50%) scaleX(2.2);
|
||||
z-index: 1;
|
||||
}
|
||||
.dash-tl-eco--left .dash-tl-eco__tick:hover { transform-origin: left center; }
|
||||
.dash-tl-eco--right .dash-tl-eco__tick:hover { transform-origin: right center; }
|
||||
|
||||
@keyframes dash-tl-eco-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary-color, #6366f1) 0%, transparent);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 10px 1px color-mix(in srgb, var(--primary-color, #6366f1) 28%, transparent);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user