🌊 React Flow + Zustand: estado mínimo, render explícito
React Flow entrega o canvas: pan, zoom, edges curvas, custom node types. Zustand guarda o estado global em slices finos: nó selecionado, persona, viewer aberto, filtro. Sem provider hell, sem Redux Toolkit, sem boilerplate.
🎯 Anatomia do store
Slices independentes que se compõem num único store:
- •
graphSlice— nós, arestas, validação - •
selectionSlice— nó selecionado, histórico de navegação - •
viewerSlice— aberto/fechado, expandido (modal), arquivo atual - •
personaSlice— Explore, Learn, Compose - •
searchSlice— query, resultados, índices
store/index.ts (esqueleto)
export const useStore = create<Store>()((set, get, api) => ({
...graphSlice(set, get, api),
...selectionSlice(set, get, api),
...viewerSlice(set, get, api),
...personaSlice(set, get, api),
...searchSlice(set, get, api),
}));
Slices
Estado modular
Selectors
Re-render seletivo
Custom nodes
React Flow types
Memo
Evitar re-mount
🔐 /file-content.json: servir arquivo sem dar tiro no pé
O dev server expõe um único endpoint: /file-content.json?path=.... Ele retorna o conteúdo do arquivo
— mas com duas camadas de defesa: access token de sessão + path allowlist
derivada do grafo. Sem token = 401. Path fora da allowlist = 403.
🚨 Por que NÃO confiar só no path?
Sem allowlist, qualquer um com acesso ao server pode fazer ?path=../../../etc/passwd. Validar "começa com cwd" é frágil — symlinks, caminhos canônicos, edge cases sem fim.
A allowlist derivada do grafo resolve: só serve o que aparece como nó file no knowledge-graph.json. Defense in depth.
Token gerado no boot
Random 32 bytes
Quando /understand-dashboard roda, gera um token único por sessão e injeta no HTML inicial. Browser usa em todas as fetches.
Allowlist construída do grafo
Set de paths normalizados
Server carrega o grafo e monta um Set<string> com os paths absolutos canônicos. Lookup O(1) por request.
Request validado em duas etapas
Token primeiro, path depois
Ordem importa: rejeita sem token antes de tocar no filesystem. Path normaliza com path.resolve, checa no Set.
Access token
Por sessão
Path allowlist
Derivada do grafo
Defense in depth
Duas camadas
Canonical paths
Mata symlink trick
🗂️ Sidebar Info / Files: duas abas, um objetivo
A sidebar de 360px à direita tem só duas abas. Simplicidade é design. Cada aba muda dentro de si conforme o contexto.
📋 Aba Info
- Default: ProjectOverview — stats do grafo, layers, top-deps
- Quando seleciona nó: NodeInfo — sumário, relações, tour stops
- Persona Learn: LearnPanel — tour interativo no contexto do nó
📁 Aba Files
- FileExplorer derivado dos nós
kind: 'file'do grafo - Árvore colapsável, ícone por linguagem
- Click no arquivo = seleciona nó no grafo + abre code viewer
💡 Dica prática
Trocar de aba não perde a seleção. NodeInfo continua mostrando o nó selecionado mesmo se você for até Files e voltar. Isso é decisão consciente: persona Learn precisa transitar entre os dois sem reset.
360px
Largura fixa
ProjectOverview
Default da Info
FileExplorer
Tree do grafo
Persona
Explore/Learn/Compose
📖 Code viewer: slide-up, prism, modal opcional
Clicou num nó tipo file? Um painel sobe pela base da tela. Quer ver fullscreen? Botão de expand promove pra modal.
Por dentro: prism-react-renderer faz syntax highlight leve, conteúdo vem via /file-content.json.
Sem Monaco, sem 2 MB de bundle extra, sem features de IDE que ninguém usa em "ler-não-editar".
✓ O que tem
- ✓Syntax highlight (prism, ~30kb)
- ✓Slide-up transition (CSS, sem libs)
- ✓Botão expand → modal full-screen
- ✓Line numbers + highlight do range relevante
- ✓Fetch gated com access token
✗ O que NÃO tem
- ✗Edição (read-only por design)
- ✗Monaco / VS Code semantics
- ✗LSP / IntelliSense
- ✗Múltiplas tabs/buffers
- ✗Git diff inline
📊 Por que Monaco saiu
- Bundle: Monaco ~2 MB gzipped; prism-react-renderer ~30 KB
- Contexto: usuário quer entender, não editar — features de IDE atrapalham
- Manutenção: theme tokens duplos (Monaco + Tailwind) viram fonte de bugs
- Performance: Monaco demora pra montar em arquivos pequenos — overhead inútil
prism
Highlight leve
Slide-up
Transição CSS
Modal escalation
Mesmo componente
Read-only
Por design
🎭 Dark luxury: preto profundo, ouro contido
Identidade visual não é decoração. Preto #0a0a0a faz o grafo brilhar.
Acento gold #d4a574 marca o que importa sem gritar. DM Serif Display
nos títulos sinaliza autoridade; Inter no resto entrega utilidade.
🎨 Design tokens (Tailwind v4)
- •
--color-bg-primary=#0a0a0a(canvas) - •
--color-accent=#d4a574(gold/amber) - •
--font-display= DM Serif Display - •
--font-body= Inter - •Tudo em
@themecentralizado (Tailwind v4 inline)
💡 Dica prática
Tailwind v4 deixa você definir tokens direto no CSS sem tailwind.config.js. Centralizou? Trocar a paleta inteira é uma única alteração em styles/theme.css. Evita o pesadelo de "achar todos os hex hard-coded".
#0a0a0a
Black canvas
#d4a574
Gold accent
DM Serif
Display font
Inter
Body font
⚠️ Schema validation no load: banner antes de tela em branco
O grafo é JSON — formato flexível, prazeroso para gerar, perigoso para confiar. Por isso, ao carregar
knowledge-graph.json, o dashboard valida contra o schema exportado pelo core. Falha = banner explicando o quê.
🚨 Cenários típicos
- Versão do schema avançou: grafo antigo + dashboard novo. Banner: "regenere com
/understand". - Edição manual quebrada: alguém abriu o JSON pra "ajustar" e errou um campo.
- Merge conflict mal resolvido: chaves duplicadas, sintaxe inválida.
- Aresta órfã: source/target apontando pra nó que sumiu.
Pseudocódigo do load
const raw = await fetch('/knowledge-graph.json').then(r => r.json());
const result = graphSchema.safeParse(raw);
if (!result.success) {
store.setError({
title: 'Knowledge graph inválido',
message: result.error.issues[0].message,
path: result.error.issues[0].path.join('.'),
suggestion: 'Rode /understand novamente.'
});
return;
}
store.setGraph(result.data);
💡 Dica prática
Banner com mensagem acionável economiza ticket de suporte. "Esquema inválido em nodes[42].kind — esperava 'file'|'function'|'class', recebeu 'fucntion'" é dez vezes mais útil que "Erro ao carregar".
Zod schema
Exportado pelo core
safeParse
Sem throw
Error banner
Topo da tela
Mensagem útil
Path + sugestão
📋 Resumo do Módulo
Fim da Trilha 2:
Você já entende a arquitetura completa do Understand Anything. Avance para a Trilha 3 (Avançado) ou volte para revisitar.