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:
julio
2026-10-06 19:12:38 -03:00
co-authored by Claude Opus 5.5
parent 9e09df40e2
commit 48fd51e377
17 changed files with 661 additions and 27 deletions
+62
View File
@@ -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>
+5 -2
View File
@@ -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}
+9 -2
View File
@@ -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;
+75
View File
@@ -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>
+24 -5
View File
@@ -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);