- web/ com Vite + Svelte 5 + TypeScript; servidor entrega web/dist em "/" (telas antigas seguem em /dashboard, /painel, /braco...) - design system em tokens CSS, tema escuro e claro, fontes locais - estado unico: foto de /estado + um EventSource em /eventos, sem polling - shell com navegacao lateral/inferior, selos de status reais e E-Stop fixo - command palette (Ctrl+K), gestos favoritos com atalhos 1-9, g+letra navega - Ao vivo: camera WebRTC e rastreamento portados para TS, rosto, aparencia, sensores do celular, IA local e feed de acontecimentos - Ajustes: campos do celular agrupados com busca, salvamento otimista e restaurar padrao (GET /app/padrao); motor da conversa, Gemini e token - carinha.html ganha modo ?painel=1 dirigido por postMessage (sem polling) - teste Playwright de ponta a ponta do painel novo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
2.0 KiB
Svelte
88 lines
2.0 KiB
Svelte
<script lang="ts">
|
|
import { ChevronDown, ChevronUp } from '@lucide/svelte';
|
|
import { estado } from '../estado.svelte';
|
|
import RostoPrevia from './RostoPrevia.svelte';
|
|
|
|
// Um só mini-rosto em todas as telas (exceto Ao vivo, que já tem o rosto grande).
|
|
let aberto = $state(true);
|
|
try {
|
|
aberto = localStorage.getItem('carinha_mini') !== '0';
|
|
} catch {
|
|
/* ok */
|
|
}
|
|
function alternar() {
|
|
aberto = !aberto;
|
|
try {
|
|
localStorage.setItem('carinha_mini', aberto ? '1' : '0');
|
|
} catch {
|
|
/* ok */
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<aside class="mini" class:aberto aria-label="Rosto ao vivo">
|
|
<button class="barra" onclick={alternar} aria-expanded={aberto}>
|
|
<span class="ponto" class:online={estado.celularOnline} aria-hidden="true"></span>
|
|
<span>Rosto {estado.celularOnline ? 'ao vivo' : '· celular offline'}</span>
|
|
{#if aberto}<ChevronDown size={16} />{:else}<ChevronUp size={16} />{/if}
|
|
</button>
|
|
{#if aberto}
|
|
<div class="tela"><RostoPrevia /></div>
|
|
{/if}
|
|
</aside>
|
|
|
|
<style>
|
|
.mini {
|
|
position: fixed;
|
|
z-index: 55;
|
|
right: var(--e-4);
|
|
bottom: calc(84px + env(safe-area-inset-bottom, 0px));
|
|
width: 220px;
|
|
border-radius: var(--raio);
|
|
overflow: hidden;
|
|
background: var(--vidro);
|
|
backdrop-filter: blur(16px);
|
|
border: 1px solid var(--borda-forte);
|
|
box-shadow: var(--sombra-flutuante);
|
|
}
|
|
.barra {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--e-2);
|
|
padding: var(--e-2) var(--e-3);
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--texto-2);
|
|
font: 500 var(--t-xs) var(--fonte);
|
|
cursor: pointer;
|
|
}
|
|
.barra span:nth-child(2) {
|
|
flex: 1;
|
|
text-align: left;
|
|
}
|
|
.ponto {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--texto-3);
|
|
}
|
|
.ponto.online {
|
|
background: var(--ok);
|
|
}
|
|
.tela {
|
|
aspect-ratio: 16 / 9;
|
|
}
|
|
@media (max-width: 639px) {
|
|
.mini {
|
|
width: 160px;
|
|
}
|
|
}
|
|
@media (min-width: 1024px) {
|
|
.mini {
|
|
bottom: var(--e-4);
|
|
width: 260px;
|
|
}
|
|
}
|
|
</style>
|