MÓDULO 2.3

🎨 O dashboard: React Flow, Zustand, sidebar e code viewer

O front-end é onde o usuário vive. Como o grafo renderiza, como o estado é organizado, como arquivos são servidos com segurança e como o viewer slide-up vira modal full-screen — sem Monaco, sem ChatPanel.

6
Tópicos
40
Minutos
Intermediário
Nível
Frontend
Tipo
Animação do dashboard mostrando o grafo estrutural sendo navegado: pan, zoom, seleção de nós e sidebar reagindo
O dashboard em ação: navegação no grafo estrutural, seleção de nós e a sidebar reagindo em tempo real. É exatamente o que você vai destrinchar abaixo.
Layout do dashboard 75% grafo + sidebar 360px · viewer sobe pela base Graph canvas (React Flow) pan · zoom · seleção · layout pluggable Sidebar 360px Info Files NodeInfo name: dashboard.tsx layer: UI deps: 3 imports LearnPanel (persona Learn) tour ativo · próximo stop explicação contextual Code viewer (prism-react-renderer) ⬆ slide-up ao clicar em file node · botão expande para modal full-screen ▲ sobe pela base ≈ 75% da largura 360px fixos
Wireframe do dashboard: canvas do grafo ocupa 75% da largura, sidebar de 360px com abas Info e Files, e o code viewer que sobe pela base ao clicar em um file node.
1

🌊 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

2

🔐 /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.

1

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.

2

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.

3

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

3

🗂️ 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

4

📖 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

5

🎭 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 @theme centralizado (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

6

⚠️ 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

React Flow + Zustand em slices — estado modular, sem boilerplate.
/file-content.json é gated — token + allowlist derivada do grafo.
Sidebar de 360px com 2 abas — Info compõe; Files deriva do grafo.
Code viewer slide-up — prism leve, modal opcional, sem Monaco.
Dark luxury via tokens — Tailwind v4, theme em um único arquivo.
Schema valida no load — banner acionável, nunca tela em branco.

Fim da Trilha 2:

Você já entende a arquitetura completa do Understand Anything. Avance para a Trilha 3 (Avançado) ou volte para revisitar.