feat(web): guia do painel com botão ⓘ em cada seção
Um ⓘ ao lado do título de cada seção (Ao vivo, Vida, Eventos, Ajustes, Reações, Conversa, Movimento) abre a explicação daquela parte: o que faz cada opção, onde fica e uma dica. No cartão Vida também há ⓘ para humores, cuidados e ritmo. O ⓘ do topo explica a tela atual. A página Guia (#/guia) junta todos os 21 tópicos em ordem de tutorial, com índice e busca; cada tópico também aparece no Ctrl+K e há atalho em Ajustes › Sistema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+12
-2
@@ -18,6 +18,9 @@
|
||||
import Reacoes from './telas/Reacoes.svelte';
|
||||
import Rosto from './telas/Rosto.svelte';
|
||||
import Eventos from './telas/Eventos.svelte';
|
||||
import Guia from './telas/Guia.svelte';
|
||||
import Ajuda from './lib/componentes/Ajuda.svelte';
|
||||
import { TOPICO_DA_TELA } from './lib/guia';
|
||||
|
||||
const TELAS: Record<Rota, Component> = {
|
||||
'ao-vivo': AoVivo,
|
||||
@@ -27,6 +30,7 @@
|
||||
movimento: Movimento,
|
||||
ajustes: Ajustes,
|
||||
rosto: Rosto,
|
||||
guia: Guia,
|
||||
};
|
||||
const ICONES: Record<string, Component<any>> = {
|
||||
'ao-vivo': Video,
|
||||
@@ -141,7 +145,10 @@
|
||||
</nav>
|
||||
|
||||
<header class="topo">
|
||||
<h1 class="titulo">{tituloDa(nav.atual)}</h1>
|
||||
<h1 class="titulo">
|
||||
{tituloDa(nav.atual)}
|
||||
{#key nav.atual}<Ajuda topico={TOPICO_DA_TELA[nav.atual] ?? 'inicio'} tamanho={17} />{/key}
|
||||
</h1>
|
||||
<div class="status linha">
|
||||
<Selo tom={estado.conexao === 'aberta' ? 'ok' : estado.conexao === 'conectando' ? 'aviso' : 'perigo'}>
|
||||
{conexaoTexto}
|
||||
@@ -172,7 +179,7 @@
|
||||
<Tela />
|
||||
</main>
|
||||
|
||||
{#if nav.atual !== 'ao-vivo' && nav.atual !== 'reacoes' && nav.atual !== 'rosto'}
|
||||
{#if nav.atual !== 'ao-vivo' && nav.atual !== 'reacoes' && nav.atual !== 'rosto' && nav.atual !== 'guia'}
|
||||
<MiniRosto />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -241,6 +248,9 @@
|
||||
border-bottom: 1px solid var(--borda);
|
||||
}
|
||||
.titulo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: var(--t-lg);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@ export interface Campo {
|
||||
opcoes?: Opcoes;
|
||||
}
|
||||
|
||||
export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] = [
|
||||
/** `ajuda` = tópico do guia (ⓘ ao lado do título). */
|
||||
export const GRUPOS: { titulo: string; descricao: string; ajuda: string; campos: Campo[] }[] = [
|
||||
{
|
||||
titulo: 'Tela e personalidade',
|
||||
ajuda: 'tela',
|
||||
descricao: 'Como o rosto aparece e se comporta sozinho.',
|
||||
campos: [
|
||||
{ chave: 'auto', rotulo: 'Gestos espontâneos', ajuda: 'Pisca e olha em volta sem comando', tipo: 'toggle' },
|
||||
@@ -61,6 +63,7 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
|
||||
},
|
||||
{
|
||||
titulo: 'Sensores e reações',
|
||||
ajuda: 'sensores',
|
||||
descricao: 'O que o celular percebe e como reage.',
|
||||
campos: [
|
||||
{ chave: 'sensorMic', rotulo: 'Microfone', ajuda: 'Só o nível de som é usado, nunca o áudio', tipo: 'toggle' },
|
||||
@@ -75,6 +78,7 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
|
||||
},
|
||||
{
|
||||
titulo: 'Sono e madrugada',
|
||||
ajuda: 'sono',
|
||||
descricao: 'O que acorda o rosto enquanto ele dorme.',
|
||||
campos: [
|
||||
{ chave: 'somAcordar', rotulo: 'Sons para acordar', ajuda: 'Quantos barulhos em 1 minuto para ele acordar', tipo: 'slider', min: 1, max: 10, passo: 1 },
|
||||
@@ -90,6 +94,7 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
|
||||
},
|
||||
{
|
||||
titulo: 'Vídeo e rastreamento',
|
||||
ajuda: 'camera',
|
||||
descricao: 'Qualidade da transmissão e rastreamento feito neste PC.',
|
||||
campos: [
|
||||
{ chave: 'cameraLado', rotulo: 'Câmera', tipo: 'escolha', opcoes: [['user', 'Frontal'], ['environment', 'Traseira']] },
|
||||
@@ -113,6 +118,7 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
|
||||
},
|
||||
{
|
||||
titulo: 'Sensibilidade (avançado)',
|
||||
ajuda: 'sensibilidade',
|
||||
descricao: 'Limiares dos sustos e intervalos entre reações.',
|
||||
campos: [
|
||||
{ chave: 'somlim', rotulo: 'Nível para perceber som', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { BookOpen, Info } from '@lucide/svelte';
|
||||
import { TOPICOS } from '../guia';
|
||||
import Modal from './Modal.svelte';
|
||||
import TopicoGuia from './TopicoGuia.svelte';
|
||||
|
||||
// Botão ⓘ: abre a explicação de um tópico do guia num balão, com atalho para o guia completo.
|
||||
let { topico, tamanho = 15 }: { topico: string; tamanho?: number } = $props();
|
||||
const t = $derived(TOPICOS[topico]);
|
||||
let aberto = $state(false);
|
||||
</script>
|
||||
|
||||
{#if t}
|
||||
<button
|
||||
type="button"
|
||||
class="ajuda"
|
||||
aria-label={'O que é: ' + t.titulo}
|
||||
title="O que é isto?"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
aberto = true;
|
||||
}}
|
||||
>
|
||||
<Info size={tamanho} />
|
||||
</button>
|
||||
<Modal bind:aberto titulo={t.emoji + ' ' + t.titulo}>
|
||||
<TopicoGuia topico={t} />
|
||||
<a class="btn pequeno guia" href={'#/guia?t=' + topico} onclick={() => (aberto = false)}>
|
||||
<BookOpen size={14} /> Ver o guia completo
|
||||
</a>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ajuda {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--texto-3);
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.ajuda:hover {
|
||||
background: var(--superficie-2);
|
||||
color: var(--destaque);
|
||||
}
|
||||
.ajuda:focus-visible {
|
||||
outline: 2px solid var(--destaque);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.guia {
|
||||
justify-self: start;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
let { titulo = '', descricao = '', acoes, children, class: classe = '' }: {
|
||||
import Ajuda from './Ajuda.svelte';
|
||||
let { titulo = '', descricao = '', ajuda = '', acoes, children, class: classe = '' }: {
|
||||
titulo?: string;
|
||||
descricao?: string;
|
||||
/** Tópico do guia: mostra um ⓘ ao lado do título. */
|
||||
ajuda?: string;
|
||||
acoes?: Snippet;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
@@ -13,7 +16,7 @@
|
||||
{#if titulo || acoes}
|
||||
<header>
|
||||
<div>
|
||||
{#if titulo}<h2>{titulo}</h2>{/if}
|
||||
{#if titulo}<h2>{titulo}{#if ajuda}<Ajuda topico={ajuda} />{/if}</h2>{/if}
|
||||
{#if descricao}<p class="fraco pequeno-texto">{descricao}</p>{/if}
|
||||
</div>
|
||||
{#if acoes}<div class="linha">{@render acoes()}</div>{/if}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Ajuda from './Ajuda.svelte';
|
||||
// Lista agrupada no estilo dos Ajustes do iPhone: título pequeno, cartão arredondado, rodapé de ajuda.
|
||||
let { titulo = '', rodape = '', children }: { titulo?: string; rodape?: string; children: Snippet } = $props();
|
||||
// `ajuda` = tópico do guia; aparece um ⓘ ao lado do título.
|
||||
let { titulo = '', rodape = '', ajuda = '', children }: { titulo?: string; rodape?: string; ajuda?: string; children: Snippet } =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<section class="grupo">
|
||||
{#if titulo}<h2>{titulo}</h2>{/if}
|
||||
{#if titulo}<h2>{titulo}{#if ajuda}<Ajuda topico={ajuda} />{/if}</h2>{/if}
|
||||
<div class="itens">{@render children()}</div>
|
||||
{#if rodape}<p class="rodape">{rodape}</p>{/if}
|
||||
</section>
|
||||
@@ -17,6 +20,10 @@
|
||||
min-width: 0;
|
||||
}
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-height: 26px;
|
||||
padding: 0 var(--e-4);
|
||||
font-size: var(--t-xs);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import type { TopicoGuia } from '../guia';
|
||||
// Conteúdo de um tópico do guia (no balão do ⓘ e na página Guia).
|
||||
let { topico }: { topico: TopicoGuia } = $props();
|
||||
</script>
|
||||
|
||||
<div class="topico">
|
||||
<p class="onde">{topico.onde}</p>
|
||||
<p class="resumo">{topico.resumo}</p>
|
||||
{#if topico.itens?.length}
|
||||
<dl>
|
||||
{#each topico.itens as item (item.nome)}
|
||||
<div class="item">
|
||||
<dt>
|
||||
{#if item.emoji}<span class="emoji" aria-hidden="true">{item.emoji}</span>{/if}
|
||||
{item.nome}
|
||||
</dt>
|
||||
<dd>{item.texto}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
</dl>
|
||||
{/if}
|
||||
{#if topico.dica}<p class="dica"><strong>Dica:</strong> {topico.dica}</p>{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topico {
|
||||
display: grid;
|
||||
gap: var(--e-3);
|
||||
}
|
||||
.onde {
|
||||
font-size: var(--t-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--texto-3);
|
||||
}
|
||||
.resumo {
|
||||
color: var(--texto);
|
||||
line-height: 1.5;
|
||||
max-width: 65ch;
|
||||
}
|
||||
dl {
|
||||
display: grid;
|
||||
gap: var(--e-3);
|
||||
margin: 0;
|
||||
}
|
||||
.item {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
dt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--e-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
.emoji {
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
dd {
|
||||
margin: 0 0 0 calc(1.4em + var(--e-2));
|
||||
color: var(--texto-2);
|
||||
font-size: var(--t-sm);
|
||||
line-height: 1.45;
|
||||
max-width: 65ch;
|
||||
}
|
||||
.dica {
|
||||
padding: var(--e-2) var(--e-3);
|
||||
border-radius: var(--raio-sm);
|
||||
background: var(--destaque-suave);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Gauge, Moon, Sparkles } from '@lucide/svelte';
|
||||
import { estado } from '../estado.svelte';
|
||||
import { configurarVida, cuidar, CUIDADOS, NECESSIDADES } from '../vida.svelte';
|
||||
import Ajuda from './Ajuda.svelte';
|
||||
import Grupo from './Grupo.svelte';
|
||||
import Interruptor from './Interruptor.svelte';
|
||||
import Linha from './Linha.svelte';
|
||||
@@ -10,11 +11,11 @@
|
||||
const v = $derived(estado.vida);
|
||||
let diarioAberto = $state(false);
|
||||
|
||||
// O ritmo também é a sensibilidade dos sensores no celular.
|
||||
// O ritmo também é a sensibilidade dos sensores no celular (detalhes no ⓘ).
|
||||
const RITMOS = [
|
||||
{ valor: 'calmo', nome: 'Calmo', descricao: 'Age pouco; só reage e acorda com vários sons ou movimentos seguidos' },
|
||||
{ valor: 'normal', nome: 'Normal', descricao: 'Reage a barulho alto e movimento forte' },
|
||||
{ valor: 'agitado', nome: 'Agitado', descricao: 'Age bastante e reage a qualquer som ou movimento' },
|
||||
{ valor: 'calmo', nome: 'Calmo', descricao: 'Reage pouco' },
|
||||
{ valor: 'normal', nome: 'Normal', descricao: 'Equilibrado' },
|
||||
{ valor: 'agitado', nome: 'Agitado', descricao: 'Reage a tudo' },
|
||||
] as const;
|
||||
|
||||
const MOTIVOS: Record<string, string> = {
|
||||
@@ -44,6 +45,7 @@
|
||||
|
||||
<Grupo
|
||||
titulo="Vida"
|
||||
ajuda="vida"
|
||||
rodape={v?.config.ligado
|
||||
? 'Ele decide sozinho o que fazer pelo humor. Tocar ou deslizar o dedo na tela do celular também conta como carinho.'
|
||||
: 'Ligado, ele tem necessidades, humor, cochilos e inventa figurinhas, como um bichinho virtual.'}
|
||||
@@ -54,7 +56,7 @@
|
||||
<div class="bicho" class:dormindo={v.dormindo}>
|
||||
<span class="humor" aria-hidden="true">{v.config.ligado ? v.emoji : '🤖'}</span>
|
||||
<div class="quem">
|
||||
<strong>{v.config.ligado ? v.nome_humor : 'Modo autônomo desligado'}</strong>
|
||||
<strong>{v.config.ligado ? v.nome_humor : 'Modo autônomo desligado'}{#if v.config.ligado}<Ajuda topico="humores" />{/if}</strong>
|
||||
<span>{idade(v.idade_dias)}{v.config.ligado && v.diario[0] ? ' · ' + v.diario[0].texto : ''}</span>
|
||||
</div>
|
||||
<Interruptor rotulo="Modo autônomo" checked={v.config.ligado} onchange={(l) => configurarVida({ ligado: l })} />
|
||||
@@ -82,6 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="cuidados">
|
||||
<span class="cuidar">Cuidar <Ajuda topico="cuidados" /></span>
|
||||
{#each CUIDADOS as c (c.acao)}
|
||||
<button class="btn pequeno" onclick={() => cuidar(c.acao)}><span aria-hidden="true">{c.emoji}</span> {c.nome}</button>
|
||||
{/each}
|
||||
@@ -99,6 +102,7 @@
|
||||
</Linha>
|
||||
<Linha icone={Gauge} cor="#ff9500" rotulo="Ritmo" detalhe={RITMOS.find((r) => r.valor === v.config.ritmo)?.descricao ?? ''}>
|
||||
{#snippet acessorio()}
|
||||
<Ajuda topico="ritmo" />
|
||||
<span class="segmentos" role="radiogroup" aria-label="Ritmo">
|
||||
{#each RITMOS as r (r.valor)}
|
||||
<button
|
||||
@@ -173,8 +177,23 @@
|
||||
min-width: 0;
|
||||
}
|
||||
.quem strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: var(--t-md);
|
||||
}
|
||||
.cuidar {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-bottom: -4px;
|
||||
font-size: var(--t-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--texto-3);
|
||||
}
|
||||
.quem span {
|
||||
color: var(--texto-3);
|
||||
font-size: var(--t-xs);
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Guia do painel: o que cada parte faz, em linguagem simples. Usado pelo botão ⓘ ao lado dos
|
||||
* títulos (abre só o tópico) e pela página Guia (todos os tópicos, como um tutorial).
|
||||
*/
|
||||
|
||||
export interface ItemGuia {
|
||||
nome: string;
|
||||
texto: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export interface TopicoGuia {
|
||||
titulo: string;
|
||||
emoji: string;
|
||||
/** Onde fica no painel. */
|
||||
onde: string;
|
||||
resumo: string;
|
||||
itens?: ItemGuia[];
|
||||
dica?: string;
|
||||
}
|
||||
|
||||
export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
inicio: {
|
||||
titulo: 'Primeiros passos',
|
||||
emoji: '👋',
|
||||
onde: 'Todo o painel',
|
||||
resumo:
|
||||
'O Carinha é um rosto de robô no celular (S10), ligado a esta placa (Orange Pi) que também move o braço. O painel mostra o que ele vê e sente, e deixa você mandar gestos, mudar o rosto e cuidar dele.',
|
||||
itens: [
|
||||
{ emoji: '📹', nome: 'Ao vivo', texto: 'Câmera do celular, rosto ao vivo, modo vida, gestos e o que está acontecendo agora.' },
|
||||
{ emoji: '🔔', nome: 'Eventos', texto: 'Histórico de presença, movimento e som, com foto de cada um.' },
|
||||
{ emoji: '💬', nome: 'Conversa', texto: 'Converse por texto ou voz; ele responde falando e com um gesto.' },
|
||||
{ emoji: '😊', nome: 'Reações', texto: 'Biblioteca de animações e figurinhas, e criação de novas com IA.' },
|
||||
{ emoji: '✋', nome: 'Movimento', texto: 'Controle do braço robótico, calibração e teste dos motores.' },
|
||||
{ emoji: '⚙️', nome: 'Ajustes', texto: 'Tudo do celular (tela, sensores, sono, vídeo), IA, segurança e sistema.' },
|
||||
{ emoji: '🛑', nome: 'Parar tudo (E-Stop)', texto: 'O botão vermelho no topo para o braço, os motores e as animações na hora.' },
|
||||
],
|
||||
dica: 'O ⓘ ao lado de cada título explica só aquela parte. Ctrl+K abre a busca de comandos.',
|
||||
},
|
||||
vida: {
|
||||
titulo: 'Modo vida (autônomo)',
|
||||
emoji: '🐣',
|
||||
onde: 'Ao vivo › Vida',
|
||||
resumo:
|
||||
'Com o modo vida ligado, o Carinha funciona como um bichinho virtual: tem necessidades que mudam com o tempo e com o que acontece em volta, e decide sozinho o que fazer pelo humor.',
|
||||
itens: [
|
||||
{ emoji: '⚡', nome: 'Energia', texto: 'Cai enquanto ele está acordado e enche dormindo. Muito baixa, ele vai tirar um cochilo.' },
|
||||
{ emoji: '😊', nome: 'Alegria', texto: 'Acompanha devagar a companhia e a diversão.' },
|
||||
{ emoji: '💬', nome: 'Companhia', texto: 'Sobe quando alguém aparece (PIR), conversa com ele ou faz carinho. Baixa, ele fica carente.' },
|
||||
{ emoji: '🎈', nome: 'Diversão', texto: 'Sobe com movimento em volta, brincadeiras e quando ele inventa algo. Baixa, ele fica entediado.' },
|
||||
{ emoji: '🎨', nome: 'Inventar figurinhas sozinho', texto: 'De vez em quando ele tem uma ideia e desenha uma figurinha nova com o Gemini, que vai para a biblioteca de Reações. No máximo 3 por dia, com 90 min entre elas.' },
|
||||
{ emoji: '🌙', nome: 'Dormir à noite', texto: 'Na madrugada ele vai dormir mesmo com gente se mexendo por perto. Carinho ou conversa seguram ele acordado por 10 minutos.' },
|
||||
{ emoji: '📖', nome: 'Diário', texto: 'Lista do que ele fez sozinho e dos cuidados que recebeu.' },
|
||||
],
|
||||
dica: 'Desligar o modo vida volta o celular aos gestos soltos de sempre e à sensibilidade dos Ajustes.',
|
||||
},
|
||||
cuidados: {
|
||||
titulo: 'Cuidar do Carinha',
|
||||
emoji: '❤️',
|
||||
onde: 'Ao vivo › Vida (botões) e a tela do celular',
|
||||
resumo: 'Os botões do cartão Vida funcionam como os de um Tamagotchi. Cada um mexe nas necessidades e ele reage na hora.',
|
||||
itens: [
|
||||
{ emoji: '❤️', nome: 'Carinho', texto: 'Companhia +15 e alegria +10. Ele faz coração.' },
|
||||
{ emoji: '🎈', nome: 'Brincar', texto: 'Diversão +25 e alegria +8, gasta um pouco de energia. Ele fica alegre.' },
|
||||
{ emoji: '⚡', nome: 'Energia', texto: 'Energia +30, como se fosse comida. Ele sorri.' },
|
||||
{ emoji: '🌙', nome: 'Ninar / ☀️ Acordar', texto: 'Põe para dormir ou acorda na hora.' },
|
||||
{ emoji: '👆', nome: 'Na tela do celular', texto: 'Deslizar o dedo é carinho; tocar é uma cutucada (um pouco de companhia e diversão). Tocar com ele dormindo acorda.' },
|
||||
],
|
||||
},
|
||||
humores: {
|
||||
titulo: 'Humores',
|
||||
emoji: '🙂',
|
||||
onde: 'Ao vivo › Vida (o emoji grande)',
|
||||
resumo: 'O humor sai das necessidades e escolhe os gestos, as expressões da boca e das sobrancelhas e o quanto ele se mexe.',
|
||||
itens: [
|
||||
{ emoji: '😄', nome: 'Feliz', texto: 'Tudo em dia. Saltita, cantarola, dá sorrisinhos e piscadinhas, mexe mais.' },
|
||||
{ emoji: '🙂', nome: 'Tranquilo', texto: 'Normal. Olha em volta, pisca devagar, meio sorriso.' },
|
||||
{ emoji: '😐', nome: 'Entediado', texto: 'Diversão baixa. Fica pensativo, boca de lado, procura o que fazer e às vezes toca uma reação da biblioteca.' },
|
||||
{ emoji: '🥺', nome: 'Carente', texto: 'Companhia baixa. Com alguém perto, acena e pede carinho; sozinho, fica cabisbaixo.' },
|
||||
{ emoji: '😢', nome: 'Triste', texto: 'Alegria baixa. Fica tristinho, olha para baixo e mexe menos.' },
|
||||
{ emoji: '🥱', nome: 'Cansado', texto: 'Energia baixa. Boceja, suspira e logo vai dormir.' },
|
||||
{ emoji: '😴', nome: 'Dormindo', texto: 'Olhos fechados, respira devagar e recupera energia.' },
|
||||
],
|
||||
},
|
||||
ritmo: {
|
||||
titulo: 'Ritmo e sensibilidade',
|
||||
emoji: '🎚️',
|
||||
onde: 'Ao vivo › Vida › Ritmo',
|
||||
resumo: 'O ritmo diz o quanto ele age sozinho e o quanto os sensores precisam ser estimulados para ele reagir ou acordar.',
|
||||
itens: [
|
||||
{ emoji: '🐢', nome: 'Calmo', texto: 'Age a cada 1 a 2,5 min. Limiares 50% mais altos; reage só no 3º barulho alto ou movimento forte seguido; acorda com o dobro de sons, 4 movimentos ou 3 disparos do PIR em 2 min.' },
|
||||
{ emoji: '🙂', nome: 'Normal', texto: 'Age a cada 25 s a 1 min. Reage a barulho alto e movimento forte; acorda com os sons configurados ou 2 movimentos.' },
|
||||
{ emoji: '⚡', nome: 'Agitado', texto: 'Age a cada 12 a 35 s. Limiares 40% mais baixos; reage a qualquer som ou movimento com reações variadas e acorda no primeiro estímulo.' },
|
||||
],
|
||||
dica: 'A madrugada silenciosa vale por cima de qualquer ritmo.',
|
||||
},
|
||||
sono: {
|
||||
titulo: 'Sono e madrugada',
|
||||
emoji: '🌙',
|
||||
onde: 'Ajustes › Sono e madrugada',
|
||||
resumo: 'Decide o que acorda o rosto enquanto ele dorme.',
|
||||
itens: [
|
||||
{ emoji: '🔊', nome: 'Sons para acordar', texto: 'Quantos barulhos em 1 minuto ele precisa ouvir para acordar. Um barulho comprido conta como um.' },
|
||||
{ emoji: '🌌', nome: 'Madrugada silenciosa', texto: 'No horário escolhido, movimento, câmera e PIR não acordam. Só barulho alto repetido, toque na tela ou conversa.' },
|
||||
{ emoji: '🕚', nome: 'Começa às / Termina às', texto: 'Horário da madrugada (padrão 23h às 7h). Pode atravessar a meia-noite.' },
|
||||
{ emoji: '⬛', nome: 'Apagar a tela quando dormir', texto: 'Fica em Tela e personalidade: depois de um tempo dormindo, a tela fica preta e o brilho vai ao mínimo para poupar bateria.' },
|
||||
],
|
||||
},
|
||||
celular: {
|
||||
titulo: 'Celular',
|
||||
emoji: '📱',
|
||||
onde: 'Ao vivo › Celular',
|
||||
resumo: 'Estado do S10: bateria, Wi-Fi, IP, versão do app e se ele está acordado. Tudo vem do próprio aplicativo.',
|
||||
itens: [
|
||||
{ emoji: '🔋', nome: 'Bateria', texto: 'Nível e se está carregando. O painel avisa quando está baixa.' },
|
||||
{ emoji: '🔄', nome: 'Recarregar tela do celular', texto: 'Recarrega o rosto no celular para pegar uma versão nova sem mexer no aparelho.' },
|
||||
{ emoji: '🕒', nome: 'Último sinal', texto: 'Se o celular sumir (bateria acabou, Wi-Fi caiu), aparece há quanto tempo ele não responde.' },
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
titulo: 'Câmera e rastreamento',
|
||||
emoji: '📹',
|
||||
onde: 'Ao vivo › Câmera e Ajustes › Vídeo e rastreamento',
|
||||
resumo: 'O vídeo vai direto do celular para este navegador (WebRTC). Nada é gravado; só a foto de cada evento fica no histórico.',
|
||||
itens: [
|
||||
{ emoji: '🎥', nome: 'Transmitir câmera do celular', texto: 'Liga o vídeo ao vivo. Toque no quadrinho do canto para trocar câmera e rosto de lugar.' },
|
||||
{ emoji: '🙂', nome: 'Rastrear rosto / mãos', texto: 'Este computador acha rostos e mãos no vídeo.' },
|
||||
{ emoji: '👀', nome: 'Olhos seguem o alvo', texto: 'Os olhos do Carinha acompanham o rosto ou a mão encontrada.' },
|
||||
{ emoji: '✏️', nome: 'Desenhar rastreamento', texto: 'Mostra no vídeo as marcações do que foi encontrado.' },
|
||||
{ emoji: '🟢', nome: 'Bolinha PIR na imagem', texto: 'Cinza com o sensor parado; verde pulsando enquanto o sensor de presença está vendo alguém.' },
|
||||
],
|
||||
},
|
||||
gestos: {
|
||||
titulo: 'Gestos',
|
||||
emoji: '😉',
|
||||
onde: 'Ao vivo › Gestos e Reações › Gestos do rosto',
|
||||
resumo: 'Animações prontas do rosto: feliz, surpreso, acenar, esconder o rosto com as mãos, dançar e outras.',
|
||||
itens: [
|
||||
{ emoji: '⭐', nome: 'Favoritos', texto: 'Até 12 gestos fixados com a estrela. Aparecem no Ao vivo.' },
|
||||
{ emoji: '⌨️', nome: 'Teclas 1 a 9', texto: 'Disparam os nove primeiros favoritos.' },
|
||||
{ emoji: '🔍', nome: 'Ctrl+K', texto: 'Busca qualquer gesto, rosto, cor ou tela pelo nome.' },
|
||||
],
|
||||
},
|
||||
aparencia: {
|
||||
titulo: 'Aparência do rosto',
|
||||
emoji: '🎨',
|
||||
onde: 'Ao vivo › Aparência › Rosto, formato e cor',
|
||||
resumo: 'Escolha o estilo do rosto, o formato dos olhos e a cor. Muda na hora no celular.',
|
||||
itens: [
|
||||
{ emoji: '👁️', nome: 'Carinha Viva', texto: 'Pálpebras, brilho e micro-movimentos.' },
|
||||
{ emoji: '🟦', nome: 'Bloco', texto: 'Olhos quadrados, estilo robô de mesa.' },
|
||||
{ emoji: '🔵', nome: 'Matriz', texto: 'Painel de LEDs; boca e sobrancelhas também em pontos.' },
|
||||
{ emoji: '🔘', nome: 'Lente', texto: 'Íris e pupila que dilata.' },
|
||||
{ emoji: '🙂', nome: 'Clássico, Miúdo e Eilik', texto: 'Os desenhos originais.' },
|
||||
{ emoji: '⬭', nome: 'Formato', texto: 'Padrão, redondo, pílula, quadrado ou largo (nos rostos novos).' },
|
||||
{ emoji: '🤨', nome: 'Microexpressões', texto: 'Acordado e sem gesto, a boca e as sobrancelhas mexem sozinhas de vez em quando (meio sorriso, sobrancelha arqueada, suspiro) conforme o humor.' },
|
||||
],
|
||||
dica: 'Boca, sobrancelhas e íris ligam e desligam em Ajustes › Tela e personalidade.',
|
||||
},
|
||||
personalidade: {
|
||||
titulo: 'IA local (Qwen)',
|
||||
emoji: '🧠',
|
||||
onde: 'Ao vivo › Personalidade',
|
||||
resumo: 'Com "Reagir sozinho com IA local" ligado, o Qwen escolhe uma expressão a partir dos sons e movimentos que o celular percebe. Roda sem internet e sem custo.',
|
||||
},
|
||||
acontecimentos: {
|
||||
titulo: 'Acontecimentos',
|
||||
emoji: '🔔',
|
||||
onde: 'Ao vivo › Acontecimentos',
|
||||
resumo: 'Lista ao vivo do que acabou de acontecer: gestos, respostas da conversa, reações, detecções e o que o modo vida fez.',
|
||||
itens: [
|
||||
{ emoji: '🚶', nome: 'Sensor de presença (PIR)', texto: 'Mostra se o sensor está funcionando e quando viu alguém pela última vez.' },
|
||||
{ emoji: '🗂️', nome: 'Eventos', texto: 'Abre o histórico completo com fotos.' },
|
||||
],
|
||||
},
|
||||
eventos: {
|
||||
titulo: 'Eventos e fotos',
|
||||
emoji: '🗂️',
|
||||
onde: 'Eventos',
|
||||
resumo: 'Cada detecção vira um evento com horário e, se ligado, uma foto da câmera do celular. Os eventos ficam guardados pelos dias escolhidos (padrão 3) e depois são apagados sozinhos.',
|
||||
itens: [
|
||||
{ emoji: '🚶', nome: 'Sensor de presença (PIR)', texto: 'Sensor HC-SR505 no pino 11 da placa. Vê calor de corpo em movimento a até ~3 m.' },
|
||||
{ emoji: '👀', nome: 'Movimento na câmera', texto: 'O celular percebe algo se mexendo na frente.' },
|
||||
{ emoji: '🔊', nome: 'Som alto', texto: 'O celular ouve um barulho forte (só o nível, nunca o áudio).' },
|
||||
{ emoji: '📷', nome: 'Guardar foto', texto: 'Tira uma foto em cada evento. "Ver foto" abre a imagem.' },
|
||||
{ emoji: '⏱️', nome: 'Intervalo', texto: 'Tempo mínimo entre dois eventos do mesmo tipo, para não lotar o histórico.' },
|
||||
],
|
||||
},
|
||||
presenca: {
|
||||
titulo: 'Modo presença',
|
||||
emoji: '🚶',
|
||||
onde: 'Eventos › Modo presença',
|
||||
resumo:
|
||||
'O PIR liga a câmera do celular quando alguém aparece e acorda o Carinha. Sem ninguém pelo tempo escolhido, a câmera desliga, ele dorme e fica em silêncio. Na madrugada silenciosa o PIR não acorda.',
|
||||
},
|
||||
tela: {
|
||||
titulo: 'Tela e personalidade',
|
||||
emoji: '📺',
|
||||
onde: 'Ajustes › Tela e personalidade',
|
||||
resumo: 'Como o rosto aparece no celular e o que ele faz sozinho.',
|
||||
itens: [
|
||||
{ emoji: '🎲', nome: 'Gestos espontâneos', texto: 'Pisca e olha em volta sem comando. Com o modo vida, os gestos seguem o humor.' },
|
||||
{ emoji: '👄', nome: 'Boca, sobrancelhas e íris', texto: 'Liga ou desliga cada parte do rosto.' },
|
||||
{ emoji: '💡', nome: 'Brilho e repouso', texto: 'Brilho normal e o mais fraco depois de um tempo sem nada acontecer.' },
|
||||
],
|
||||
},
|
||||
sensores: {
|
||||
titulo: 'Sensores e reações',
|
||||
emoji: '🎙️',
|
||||
onde: 'Ajustes › Sensores e reações',
|
||||
resumo: 'O que o celular usa para perceber o ambiente. Só os níveis são usados: nenhum áudio é gravado e a câmera não sai do celular sem você ligar a transmissão.',
|
||||
itens: [
|
||||
{ emoji: '🎙️', nome: 'Microfone / Reagir a sons', texto: 'Ouve o nível do som para se assustar e acordar.' },
|
||||
{ emoji: '🤫', nome: 'Silêncio para dormir', texto: 'Quantos segundos sem som nem movimento até ele adormecer sozinho.' },
|
||||
{ emoji: '📷', nome: 'Câmera como sensor / Reagir a movimento', texto: 'Detecta movimento na frente do celular.' },
|
||||
{ emoji: '😴', nome: 'Microfone e câmera durante o sono', texto: 'Se continuam ligados dormindo (para poder acordar).' },
|
||||
],
|
||||
},
|
||||
sensibilidade: {
|
||||
titulo: 'Sensibilidade (avançado)',
|
||||
emoji: '🎛️',
|
||||
onde: 'Ajustes › Sensibilidade',
|
||||
resumo: 'Os limiares de base dos sustos. O ritmo do modo vida multiplica esses valores (calmo sobe, agitado desce).',
|
||||
itens: [
|
||||
{ emoji: '🔉', nome: 'Nível para perceber som', texto: 'Acima disso conta como "tem som".' },
|
||||
{ emoji: '💥', nome: 'Som mínimo e aumento súbito para susto', texto: 'Precisa passar dos dois: ser alto e ser bem mais alto que o barulho de fundo.' },
|
||||
{ emoji: '🏃', nome: 'Movimento mínimo / para susto', texto: 'Quanto precisa mexer para ele olhar e para ele se assustar.' },
|
||||
{ emoji: '⏳', nome: 'Intervalos', texto: 'Tempo mínimo entre dois sustos seguidos.' },
|
||||
],
|
||||
},
|
||||
reacoes: {
|
||||
titulo: 'Reações',
|
||||
emoji: '✨',
|
||||
onde: 'Reações',
|
||||
resumo: 'Animações e figurinhas que aparecem nos olhos ou em volta do rosto.',
|
||||
itens: [
|
||||
{ emoji: '📚', nome: 'Biblioteca', texto: 'Toque para tocar no celular. Dá para curtir, editar, duplicar, desativar e apagar (com desfazer).' },
|
||||
{ emoji: '🖌️', nome: 'Criar com IA', texto: 'Descreva uma figurinha e o Gemini desenha; ou deixe ele criar 3 reações a partir da conversa e da rotina.' },
|
||||
{ emoji: '😉', nome: 'Gestos do rosto', texto: 'Todos os gestos, com a estrela para fixar nos favoritos.' },
|
||||
{ emoji: '📖', nome: 'Diário', texto: 'Resumos e registros usados para criar reações.' },
|
||||
],
|
||||
},
|
||||
conversa: {
|
||||
titulo: 'Conversa',
|
||||
emoji: '💬',
|
||||
onde: 'Conversa',
|
||||
resumo: 'Fale ou escreva; o Carinha responde em voz de robô, com legenda na tela do celular e um gesto.',
|
||||
itens: [
|
||||
{ emoji: '🤖', nome: 'Motor', texto: 'Qwen local (sem internet, sem custo) ou Gemini (responde melhor). Troca em Ajustes › Inteligência artificial.' },
|
||||
{ emoji: '🧠', nome: 'Lembranças', texto: 'Coisas que ele guarda sobre você e usa nas respostas.' },
|
||||
{ emoji: '🗓️', nome: 'Rotina e preferências', texto: 'Hábitos e gostos que você ensina; também inspiram as reações.' },
|
||||
],
|
||||
},
|
||||
movimento: {
|
||||
titulo: 'Movimento (braço)',
|
||||
emoji: '🦾',
|
||||
onde: 'Movimento',
|
||||
resumo: 'Controle do braço de 7 articulações pela placa de servos (PCA9685).',
|
||||
itens: [
|
||||
{ emoji: '✋', nome: 'Segurar para mover', texto: 'O braço só se mexe enquanto você segura o botão ou o controle. Soltou, ele para.' },
|
||||
{ emoji: '🧍', nome: 'Posturas e sequências', texto: 'Posições salvas e movimentos encadeados. A prévia anima só a tela.' },
|
||||
{ emoji: '📏', nome: 'Calibração', texto: 'Limites de cada motor, testados passo a passo.' },
|
||||
{ emoji: '🩺', nome: 'Diagnóstico', texto: 'Teste canal a canal com limites conservadores. Não abre junto com o controle do braço.' },
|
||||
],
|
||||
dica: 'O E-Stop vermelho no topo para tudo. Depois é preciso rearmar para mover de novo.',
|
||||
},
|
||||
atalhos: {
|
||||
titulo: 'Atalhos de teclado',
|
||||
emoji: '⌨️',
|
||||
onde: 'Em qualquer tela',
|
||||
resumo: 'Para usar o painel sem o mouse.',
|
||||
itens: [
|
||||
{ nome: 'Ctrl+K', texto: 'Busca de comandos (gestos, rostos, cores, telas, este guia).' },
|
||||
{ nome: '1 a 9', texto: 'Gestos favoritos.' },
|
||||
{ nome: 'Esc Esc ou Shift+Espaço', texto: 'E-Stop: para tudo.' },
|
||||
{ nome: 'g e uma letra', texto: 'Vai para uma tela: g v Ao vivo, g e Eventos, g c Conversa, g r Reações, g m Movimento, g a Ajustes.' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/** Ordem do tutorial (página Guia). */
|
||||
export const ORDEM_GUIA = [
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'aparencia',
|
||||
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||
];
|
||||
|
||||
/** Tópico de cada tela, para o ⓘ do topo. */
|
||||
export const TOPICO_DA_TELA: Record<string, string> = {
|
||||
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
|
||||
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio',
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
cor, CORES, formato, FORMATOS_INFO, gesto, GESTOS_INFO, gestosDisponiveis, nomeRosto, pararTudo, rearmar, recarregarCelular, rosto,
|
||||
} from './comandos.svelte';
|
||||
import { estado } from './estado.svelte';
|
||||
import { ORDEM_GUIA, TOPICOS } from './guia';
|
||||
import { nav, ROTAS } from './rotas.svelte';
|
||||
|
||||
export interface ItemPaleta {
|
||||
@@ -36,6 +37,13 @@ export function itensDaPaleta(): ItemPaleta[] {
|
||||
registrarNaPaleta(() => [
|
||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
||||
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
|
||||
...ORDEM_GUIA.map((t) => ({
|
||||
id: 'guia-' + t,
|
||||
grupo: 'Guia',
|
||||
titulo: TOPICOS[t].emoji + ' ' + TOPICOS[t].titulo,
|
||||
dica: TOPICOS[t].onde,
|
||||
fazer: () => (location.hash = '/guia?t=' + t),
|
||||
})),
|
||||
...estado.formatos.map((f) => ({ id: 'formato-' + f, grupo: 'Formato dos olhos', titulo: FORMATOS_INFO[f] ?? f, fazer: () => formato(f) })),
|
||||
estado.estop.ativo
|
||||
? { id: 'rearmar', grupo: 'Ações', titulo: 'Rearmar motores', fazer: rearmar }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
|
||||
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto';
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia';
|
||||
|
||||
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
|
||||
@@ -14,6 +14,7 @@ export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
/** Páginas abertas a partir de outra tela (não aparecem na navegação). */
|
||||
export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
|
||||
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
|
||||
{ id: 'guia', titulo: 'Guia', volta: 'ajustes' },
|
||||
];
|
||||
|
||||
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Download, KeyRound, RefreshCw, RotateCcw, Search, ShieldCheck, ShieldOff } from '@lucide/svelte';
|
||||
import { BookOpen, Download, KeyRound, RefreshCw, RotateCcw, Search, ShieldCheck, ShieldOff } from '@lucide/svelte';
|
||||
import { gravarToken, lerToken, postar } from '../lib/api';
|
||||
import { GRUPOS, restaurarPadrao, salvarApp, type Campo } from '../lib/ajustes.svelte';
|
||||
import { avisos, tentar } from '../lib/avisos.svelte';
|
||||
import { recarregarCelular } from '../lib/comandos.svelte';
|
||||
import Ajuda from '../lib/componentes/Ajuda.svelte';
|
||||
import Card from '../lib/componentes/Card.svelte';
|
||||
import Selo from '../lib/componentes/Selo.svelte';
|
||||
import Slider from '../lib/componentes/Slider.svelte';
|
||||
@@ -97,7 +98,7 @@
|
||||
{#each grupos as g (g.titulo)}
|
||||
<fieldset>
|
||||
<legend>
|
||||
<strong>{g.titulo}</strong>
|
||||
<strong class="titulo-grupo">{g.titulo} <Ajuda topico={g.ajuda} /></strong>
|
||||
<span class="fraco pequeno-texto">{g.descricao}</span>
|
||||
</legend>
|
||||
{#each g.campos as c (c.chave)}
|
||||
@@ -137,7 +138,7 @@
|
||||
</Card>
|
||||
|
||||
<div class="coluna">
|
||||
<Card titulo="Inteligência artificial">
|
||||
<Card titulo="Inteligência artificial" ajuda="conversa">
|
||||
<div class="campo">
|
||||
<span>Motor da conversa</span>
|
||||
<div class="segmentado" role="radiogroup" aria-label="Motor da conversa">
|
||||
@@ -223,6 +224,7 @@
|
||||
<RefreshCw size={14} /> Recarregar tela do celular
|
||||
</button>
|
||||
<a class="btn pequeno" href="/app.apk" download><Download size={14} /> Baixar app Android</a>
|
||||
<a class="btn pequeno" href="#/guia"><BookOpen size={14} /> Guia do painel</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -267,6 +269,11 @@
|
||||
gap: var(--e-2);
|
||||
align-content: start;
|
||||
}
|
||||
.titulo-grupo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
legend {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
|
||||
@@ -125,12 +125,12 @@ const pirAtivo = $derived(
|
||||
<div class="lista">
|
||||
<VidaCartao />
|
||||
|
||||
<Grupo titulo="Celular">
|
||||
<Grupo titulo="Celular" ajuda="celular">
|
||||
<Celular />
|
||||
<Linha icone={RefreshCw} cor="#0a84ff" rotulo="Recarregar tela do celular" onclick={recarregarCelular} disabled={!estado.celularOnline} />
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Câmera" rodape={transmitindo && rastreio ? rastreio.texto : 'O vídeo vai direto do celular para este navegador (WebRTC); nada é gravado.'}>
|
||||
<Grupo titulo="Câmera" ajuda="camera" rodape={transmitindo && rastreio ? rastreio.texto : 'O vídeo vai direto do celular para este navegador (WebRTC); nada é gravado.'}>
|
||||
<Linha icone={Video} cor="#34c759" rotulo="Transmitir câmera do celular" detalhe={video?.exibindo ? video.qualidade : ''}>
|
||||
{#snippet acessorio()}
|
||||
<Interruptor rotulo="Transmitir câmera do celular" checked={transmitindo} disabled={!cfg} onchange={(v) => salvarApp({ preview: v })} />
|
||||
@@ -161,14 +161,14 @@ const pirAtivo = $derived(
|
||||
<Linha icone={Settings2} cor="#8e8e93" rotulo="Qualidade, câmera frontal/traseira e sensores" href="#/ajustes" />
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Gestos" rodape="Teclas 1 a 9 disparam os nove primeiros. Ctrl+K busca qualquer gesto.">
|
||||
<Grupo titulo="Gestos" ajuda="gestos" rodape="Teclas 1 a 9 disparam os nove primeiros. Ctrl+K busca qualquer gesto.">
|
||||
<Linha icone={Smile} cor="#ffcc00" rotulo="Favoritos">
|
||||
<Gestos />
|
||||
</Linha>
|
||||
<Linha icone={Sparkles} cor="#ff2d55" rotulo="Todos os gestos e reações" href="#/reacoes" />
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Aparência">
|
||||
<Grupo titulo="Aparência" ajuda="aparencia">
|
||||
<Linha
|
||||
icone={Palette}
|
||||
cor="#ff9500"
|
||||
@@ -182,7 +182,7 @@ const pirAtivo = $derived(
|
||||
</Linha>
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Personalidade" rodape={estado.ia?.erro || 'Qwen escolhe expressões a partir de sons e movimentos do celular.'}>
|
||||
<Grupo titulo="Personalidade" ajuda="personalidade" rodape={estado.ia?.erro || 'Qwen escolhe expressões a partir de sons e movimentos do celular.'}>
|
||||
<Linha
|
||||
icone={Brain}
|
||||
cor="#5e5ce6"
|
||||
@@ -195,7 +195,7 @@ const pirAtivo = $derived(
|
||||
</Linha>
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="Acontecimentos">
|
||||
<Grupo titulo="Acontecimentos" ajuda="acontecimentos">
|
||||
<Linha icone={Footprints} cor="#34c759" rotulo="Sensor de presença (PIR)">
|
||||
{#snippet acessorio()}<PresencaPir />{/snippet}
|
||||
</Linha>
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card titulo="Memória" descricao="O que o Carinha leva em conta ao responder. São dados, nunca instruções.">
|
||||
<Card titulo="Memória" ajuda="conversa" descricao="O que o Carinha leva em conta ao responder. São dados, nunca instruções.">
|
||||
<Abas
|
||||
rotulo="Memória"
|
||||
bind:ativa={aba}
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
<aside class="lado">
|
||||
<Grupo
|
||||
titulo="Modo presença"
|
||||
ajuda="presenca"
|
||||
rodape="Alguém aparece: o PIR liga a câmera do celular e acorda o Carinha. Ninguém pelo tempo escolhido: a câmera desliga, ele dorme e fica em silêncio."
|
||||
>
|
||||
<Linha
|
||||
@@ -133,7 +134,7 @@
|
||||
</Linha>
|
||||
</Grupo>
|
||||
|
||||
<Grupo titulo="O que registrar">
|
||||
<Grupo titulo="O que registrar" ajuda="eventos">
|
||||
<Linha icone={Footprints} cor="#34c759" rotulo="Sensor de presença (PIR)" detalhe={st?.pir.disponivel ? 'Pino 11 · HC-SR505' : st?.pir.erro || 'indisponível'}>
|
||||
{#snippet acessorio()}
|
||||
<Interruptor rotulo="Sensor de presença (PIR)" checked={!!st?.config.pir} disabled={!st} onchange={(v) => historico.configurar({ pir: v })} />
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import TopicoGuia from '../lib/componentes/TopicoGuia.svelte';
|
||||
import { ORDEM_GUIA, TOPICOS } from '../lib/guia';
|
||||
|
||||
// Guia completo: todos os tópicos em ordem de tutorial, com índice. #/guia?t=vida abre no tópico.
|
||||
let busca = $state('');
|
||||
const normal = (t: string) => t.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
||||
const lista = $derived(
|
||||
ORDEM_GUIA.filter((id) => {
|
||||
if (!busca) return true;
|
||||
const t = TOPICOS[id];
|
||||
const texto = [t.titulo, t.resumo, t.onde, t.dica ?? '', ...(t.itens ?? []).map((i) => i.nome + ' ' + i.texto)].join(' ');
|
||||
return normal(texto).includes(normal(busca));
|
||||
}),
|
||||
);
|
||||
|
||||
async function irPara(id: string) {
|
||||
await tick();
|
||||
document.getElementById('guia-' + id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// Abre no tópico do endereço, também quando o ⓘ é usado com o guia já aberto.
|
||||
function pelaUrl() {
|
||||
const t = new URLSearchParams(location.hash.split('?')[1] ?? '').get('t');
|
||||
if (t && TOPICOS[t]) irPara(t);
|
||||
}
|
||||
onMount(() => {
|
||||
pelaUrl();
|
||||
addEventListener('hashchange', pelaUrl);
|
||||
return () => removeEventListener('hashchange', pelaUrl);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="guia">
|
||||
<aside class="indice">
|
||||
<label class="busca">
|
||||
<span class="sr">Buscar no guia</span>
|
||||
<input type="search" bind:value={busca} placeholder="Buscar no guia (ex.: ritmo, PIR, dormir)" />
|
||||
</label>
|
||||
<nav aria-label="Tópicos do guia">
|
||||
{#each lista as id (id)}
|
||||
<a href={'#/guia?t=' + id} onclick={(e) => { e.preventDefault(); history.replaceState(null, '', '#/guia?t=' + id); irPara(id); }}>
|
||||
<span aria-hidden="true">{TOPICOS[id].emoji}</span> {TOPICOS[id].titulo}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="topicos">
|
||||
{#each lista as id (id)}
|
||||
<section class="topico" id={'guia-' + id}>
|
||||
<h2><span aria-hidden="true">{TOPICOS[id].emoji}</span> {TOPICOS[id].titulo}</h2>
|
||||
<TopicoGuia topico={TOPICOS[id]} />
|
||||
</section>
|
||||
{:else}
|
||||
<p class="vazio">Nada no guia com “{busca}”.</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.guia {
|
||||
display: grid;
|
||||
gap: var(--e-5);
|
||||
align-items: start;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.guia {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
.indice {
|
||||
position: sticky;
|
||||
top: calc(var(--topo-altura) + var(--e-4));
|
||||
}
|
||||
}
|
||||
.indice {
|
||||
display: grid;
|
||||
gap: var(--e-3);
|
||||
}
|
||||
.sr {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--e-2);
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
nav {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
nav a {
|
||||
display: flex;
|
||||
gap: var(--e-2);
|
||||
align-items: center;
|
||||
padding: var(--e-1) var(--e-3);
|
||||
border-radius: var(--raio-pill);
|
||||
background: var(--superficie);
|
||||
border: 1px solid var(--borda);
|
||||
color: var(--texto-2);
|
||||
font-size: var(--t-sm);
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
nav a {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: var(--raio-sm);
|
||||
}
|
||||
}
|
||||
nav a:hover {
|
||||
background: var(--superficie-2);
|
||||
color: var(--texto);
|
||||
}
|
||||
.topicos {
|
||||
display: grid;
|
||||
gap: var(--e-4);
|
||||
min-width: 0;
|
||||
}
|
||||
.topico {
|
||||
display: grid;
|
||||
gap: var(--e-3);
|
||||
padding: var(--e-5);
|
||||
background: var(--superficie);
|
||||
border: 1px solid var(--borda);
|
||||
border-radius: var(--raio-lg);
|
||||
scroll-margin-top: calc(var(--topo-altura) + var(--e-4));
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--t-lg);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.vazio {
|
||||
color: var(--texto-3);
|
||||
}
|
||||
</style>
|
||||
@@ -171,7 +171,7 @@
|
||||
{/if}
|
||||
{:else if aba === 'criar'}
|
||||
<div class="grade-criar">
|
||||
<Card titulo="Desenhar figurinha" descricao="O Gemini desenha um SVG e o celular mostra assim que ficar pronto.">
|
||||
<Card titulo="Desenhar figurinha" ajuda="reacoes" descricao="O Gemini desenha um SVG e o celular mostra assim que ficar pronto.">
|
||||
<form class="grade" onsubmit={desenhar}>
|
||||
<label class="campo">
|
||||
<span>O que desenhar</span>
|
||||
@@ -204,7 +204,7 @@
|
||||
</Card>
|
||||
</div>
|
||||
{:else if aba === 'gestos'}
|
||||
<Card titulo="Gestos do rosto" descricao="Toque para mandar ao celular. A estrela fixa o gesto nos favoritos (até 12; as teclas 1 a 9 valem para os nove primeiros).">
|
||||
<Card titulo="Gestos do rosto" ajuda="gestos" descricao="Toque para mandar ao celular. A estrela fixa o gesto nos favoritos (até 12; as teclas 1 a 9 valem para os nove primeiros).">
|
||||
<Gestos completo />
|
||||
</Card>
|
||||
{:else}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</Card>
|
||||
|
||||
<div class="coluna">
|
||||
<Card titulo="Mover">
|
||||
<Card titulo="Mover" ajuda="movimento">
|
||||
<Toggle
|
||||
rotulo="Ativar movimento físico"
|
||||
ajuda={!st?.disponivel ? 'PCA9685 indisponível neste servidor' : estado.estop.ativo ? 'Rearme o E-Stop primeiro' : 'Com isso desligado, tudo acontece só na tela'}
|
||||
|
||||
Reference in New Issue
Block a user