feat(web): cria painel Svelte com shell, Ao vivo e Ajustes
- 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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
let { rotulo, valor, min, max, passo = 1, unidade = '', disabled = false, oninput, onchange }: {
|
||||
rotulo: string;
|
||||
valor: number;
|
||||
min: number;
|
||||
max: number;
|
||||
passo?: number;
|
||||
unidade?: string;
|
||||
disabled?: boolean;
|
||||
oninput?: (v: number) => void;
|
||||
onchange?: (v: number) => void;
|
||||
} = $props();
|
||||
const id = 's' + Math.random().toString(36).slice(2, 9);
|
||||
let local = $state<number | null>(null);
|
||||
const mostrado = $derived(local ?? valor);
|
||||
const casas = $derived(passo < 1 ? Math.min(2, String(passo).split('.')[1]?.length ?? 2) : 0);
|
||||
</script>
|
||||
|
||||
<div class="slider">
|
||||
<label for={id}>
|
||||
<span>{rotulo}</span>
|
||||
<output for={id} class="mono">{mostrado.toFixed(casas)}{unidade}</output>
|
||||
</label>
|
||||
<input
|
||||
{id}
|
||||
type="range"
|
||||
{min}
|
||||
{max}
|
||||
step={passo}
|
||||
value={mostrado}
|
||||
{disabled}
|
||||
style="--p: {((mostrado - min) / (max - min)) * 100}%"
|
||||
oninput={(e) => {
|
||||
local = Number(e.currentTarget.value);
|
||||
oninput?.(local);
|
||||
}}
|
||||
onchange={(e) => {
|
||||
const v = Number(e.currentTarget.value);
|
||||
local = null;
|
||||
onchange?.(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
display: grid;
|
||||
gap: var(--e-1);
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--t-sm);
|
||||
gap: var(--e-2);
|
||||
}
|
||||
output {
|
||||
color: var(--texto-2);
|
||||
}
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
input::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(to right, var(--destaque) var(--p), var(--superficie-3) var(--p));
|
||||
}
|
||||
input::-moz-range-track {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(to right, var(--destaque) var(--p), var(--superficie-3) var(--p));
|
||||
}
|
||||
input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -7px;
|
||||
border-radius: 50%;
|
||||
background: var(--texto);
|
||||
border: 3px solid var(--destaque);
|
||||
}
|
||||
input::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--texto);
|
||||
border: 3px solid var(--destaque);
|
||||
}
|
||||
input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user