feat(web): migra Conversa e Reacoes para o painel novo

- Conversa: chat empurrado por SSE, indicador de "pensando", gesto da
  resposta com repetir, voz com aviso claro quando o contexto nao e seguro
  e leitura em voz alta so de respostas novas
- Memoria unificada: lembrancas da conversa + rotina/preferencias em abas;
  apagar tudo com confirmacao inline (sem confirm nativo)
- Reacoes: biblioteca com miniaturas estaticas (sem iframe por cartao),
  busca, mostrar no celular, feedback, editar em <dialog>, duplicar,
  ativar e excluir com Desfazer no toast; criar com IA; diario; grade de
  gestos com estrela para favoritos; uma unica previa lateral
- treinamento recarrega por evento SSE ao terminar, sem polling
- corrige icones encolhidos em botoes .pequeno.icone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-05 19:51:45 -03:00
co-authored by Claude Opus 5.5
parent c4b53a8976
commit 947c0a3127
9 changed files with 1137 additions and 9 deletions
+64
View File
@@ -0,0 +1,64 @@
<script lang="ts" generics="T extends string">
let { abas, ativa = $bindable(), rotulo }: { abas: { id: T; titulo: string; contador?: number }[]; ativa: T; rotulo: string } =
$props();
function tecla(e: KeyboardEvent, i: number) {
const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
const prox = abas[(i + d + abas.length) % abas.length];
ativa = prox.id;
(e.currentTarget as HTMLElement).parentElement?.querySelector<HTMLElement>(`[data-aba="${prox.id}"]`)?.focus();
}
</script>
<div class="abas" role="tablist" aria-label={rotulo}>
{#each abas as aba, i (aba.id)}
<button
role="tab"
data-aba={aba.id}
aria-selected={ativa === aba.id}
tabindex={ativa === aba.id ? 0 : -1}
onclick={() => (ativa = aba.id)}
onkeydown={(e) => tecla(e, i)}
>
{aba.titulo}
{#if aba.contador !== undefined}<span class="contador">{aba.contador}</span>{/if}
</button>
{/each}
</div>
<style>
.abas {
display: flex;
gap: var(--e-1);
border-bottom: 1px solid var(--borda);
overflow-x: auto;
scrollbar-width: none;
}
button {
display: inline-flex;
align-items: center;
gap: var(--e-2);
padding: var(--e-2) var(--e-3);
min-height: 40px;
border: 0;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
background: transparent;
color: var(--texto-3);
font: 500 var(--t-sm) var(--fonte);
cursor: pointer;
white-space: nowrap;
}
button[aria-selected='true'] {
color: var(--texto);
border-bottom-color: var(--destaque);
}
.contador {
font-size: var(--t-xs);
padding: 0 6px;
border-radius: var(--raio-pill);
background: var(--superficie-3);
color: var(--texto-2);
}
</style>