# Relatório de Melhorias Arquiteturais

> **Objetivo:** Identificar oportunidades para melhorar a separação entre UI e lógica de negócio, tornando o projeto mais moderno, testável e manutenível.
>
> **Data da análise:** Maio 2026  
> **Escopo:** Todos os componentes, hooks e contextos do projeto

---

## Sumário Executivo

O projeto já adota boas práticas em vários pontos — o padrão **headless hook** está presente em `useRichTextEditor`, `useTreeView`, `useSidebar` e `useAssistant`. No entanto, existem **inconsistências** onde a mesma lógica é duplicada em múltiplos lugares e componentes com mais de 600 linhas misturam JSX com lógica de estado.

### Severidade dos Problemas

| Severidade | Quantidade | Exemplos                                                                                                                                |
| ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| 🔴 Crítico | 1 ~~2~~    | ~~`ThemeToggle` bypassa `ThemeContext`~~ ✅ resolvido em v2.2.0; tipos duplicados entre contextos                                       |
| 🟠 Alto    | 3          | `AudioPlayer` sem hook; detecção mobile duplicada 4×; `xertica-assistant.tsx` com 1573 linhas                                           |
| 🟡 Médio   | 4          | `sidebar.tsx` com 1089 linhas; atalhos de teclado no `LayoutContext`; `BrandColorsContext` com utilitário inline; `header.tsx` sem hook |
| 🟢 Baixo   | 3          | `SidebarTooltipContent` duplicado; `AssistantTooltipContent` inline; cookie management inline                                           |

---

## 🔴 Problemas Críticos

### C1 — `ThemeToggle` bypassa `ThemeContext` completamente ✅ RESOLVIDO (v2.2.0)

**Arquivo:** [`components/brand/theme-toggle/ThemeToggle.tsx`](../components/brand/theme-toggle/ThemeToggle.tsx)

**Resolução:** O `ThemeToggle` foi corrigido para usar `useTheme()` do `ThemeContext`. Adicionalmente, o bug `disableDarkMode` foi corrigido em `templates/src/app/App.tsx` e `bin/language-config.ts` — o prop estava hard-coded como `true`, tornando o `toggleTheme()` um no-op em projetos gerados pelo CLI. O `ThemeToggle` agora funciona corretamente tanto no projeto base quanto em projetos gerados.

<details>
<summary>Análise original (histórico)</summary>

**Problema original:** O componente `ThemeToggle` manipulava `document.documentElement.classList` diretamente e mantinha seu próprio estado local com `useState`, ignorando completamente o `ThemeContext` e o hook `useTheme()`. Isso criava um **risco de dessincronização**: se o tema fosse alterado via `ThemeContext` (ex: por outro componente ou por `defaultTheme`), o `ThemeToggle` não refletiria a mudança.

Adicionalmente, em projetos gerados pelo CLI, `XerticaProvider` era instanciado com `disableDarkMode` hard-coded, fazendo `toggleTheme()` retornar imediatamente sem efeito.

</details>

---

### C2 — Tipos duplicados entre `AssistenteContext` e `xertica-assistant.tsx`

**Arquivos:**

- [`contexts/AssistenteContext.tsx`](../contexts/AssistenteContext.tsx) — linhas 5–71
- [`components/assistant/xertica-assistant/xertica-assistant.tsx`](../components/assistant/xertica-assistant/xertica-assistant.tsx) — linhas 155–230

**Problema:** Os mesmos tipos são definidos em dois lugares com nomes e estruturas ligeiramente diferentes:

| `AssistenteContext.tsx` | `xertica-assistant.tsx` | Diferença                            |
| ----------------------- | ----------------------- | ------------------------------------ |
| `Message`               | `Message`               | Campos diferentes (`role` vs `type`) |
| `Conversa`              | `Conversation`          | Nome diferente, campos em PT vs EN   |
| `SearchResult`          | `SearchResult`          | Duplicado idêntico                   |
| `SearchSource`          | `SearchSource`          | Duplicado idêntico                   |
| `SearchCommand`         | `SearchCommand`         | Duplicado idêntico                   |

Isso significa que existem **dois contratos de tipo incompatíveis** para os mesmos dados, forçando conversões implícitas e tornando refatorações perigosas.

**Solução:** Criar um arquivo de tipos compartilhado:

```
components/assistant/xertica-assistant/
├── types.ts          ← NOVO: fonte única de verdade para todos os tipos
├── use-assistant.ts  ← importa de types.ts
├── xertica-assistant.tsx ← importa de types.ts
contexts/
└── AssistenteContext.tsx ← importa de types.ts (re-exporta para compatibilidade)
```

```ts
// components/assistant/xertica-assistant/types.ts
export interface Message { ... }
export interface Conversation { ... }
export interface SearchResult { ... }
// ...

// contexts/AssistenteContext.tsx
export type { Message, Conversation, SearchResult } from '@/components/assistant/xertica-assistant/types';
```

---

## 🟠 Problemas de Alta Prioridade

### A1 — Detecção de mobile duplicada em 4+ lugares

**Problema:** A lógica `window.innerWidth < 768` está duplicada em:

| Arquivo                                                                                                                 | Linha | Forma                            |
| ----------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------- |
| [`components/shared/use-mobile.ts`](../components/shared/use-mobile.ts)                                                 | 18    | `useIsMobile()` hook ✅          |
| [`components/assistant/xertica-assistant/use-assistant.ts`](../components/assistant/xertica-assistant/use-assistant.ts) | ~192  | `window.innerWidth < 768` inline |
| [`components/layout/sidebar/use-sidebar.ts`](../components/layout/sidebar/use-sidebar.ts)                               | ~46   | `window.innerWidth < 768` inline |
| [`components/media/audio-player/AudioPlayer.tsx`](../components/media/audio-player/AudioPlayer.tsx)                     | ~113  | `window.innerWidth < 768` inline |
| [`contexts/LayoutContext.tsx`](../contexts/LayoutContext.tsx)                                                           | ~60   | `window.innerWidth < 768` inline |

O hook `useIsMobile()` **já existe** em `components/shared/use-mobile.ts` e usa `matchMedia` (mais eficiente que `resize` events), mas não está sendo usado pelos outros arquivos.

**Solução:** Substituir todas as ocorrências pelo hook existente:

```ts
// ❌ ATUAL
const [isMobileViewport, setIsMobileViewport] = useState(window.innerWidth < 768);
useEffect(() => {
  const handleResize = () => setIsMobileViewport(window.innerWidth < 768);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

// ✅ CORRETO
import { useIsMobile } from '@/components/shared/use-mobile';
const isMobileViewport = useIsMobile();
```

**Impacto:** Reduz código duplicado, garante breakpoint consistente (768px), usa `matchMedia` que é mais performático que `resize` events.

---

### A2 — `AudioPlayer.tsx` (663 linhas) sem hook headless

**Arquivo:** [`components/media/audio-player/AudioPlayer.tsx`](../components/media/audio-player/AudioPlayer.tsx)

**Problema:** Todo o estado e lógica do player de áudio está inline no componente, misturado com JSX. Inclui:

- Gerenciamento de estado (`isPlaying`, `currentTime`, `duration`, `volume`, `playbackRate`, `isFloating`)
- `IntersectionObserver` para auto-float
- Sincronização de áudio com `useEffect`
- Detecção de mobile (duplicada)
- Formatação de tempo (`formatTime`)
- Handlers de eventos (`togglePlay`, `handleSeek`, `handleVolumeChange`, `handleSetFloating`)

**Solução:** Extrair para `use-audio-player.ts`:

```ts
// components/media/audio-player/use-audio-player.ts
export interface UseAudioPlayerProps {
  src: string;
  autoFloat?: boolean;
  onEnded?: () => void;
}

export interface UseAudioPlayerReturn {
  audioRef: React.RefObject<HTMLAudioElement>;
  containerRef: React.RefObject<HTMLDivElement>;
  isPlaying: boolean;
  currentTime: number;
  duration: number;
  volume: number;
  playbackRate: number;
  isFloating: boolean;
  isMobile: boolean;
  togglePlay: () => void;
  handleSeek: (value: number[]) => void;
  handleVolumeChange: (value: number[]) => void;
  handleSetFloating: (floating: boolean) => void;
  setPlaybackRate: (rate: number) => void;
  formatTime: (time: number) => string;
}

export function useAudioPlayer(props: UseAudioPlayerProps): UseAudioPlayerReturn { ... }
```

**Benefícios:**

- `AudioPlayer.tsx` fica com ~200 linhas de JSX puro
- Lógica do player pode ser testada unitariamente sem renderizar DOM
- Permite criar variantes do player (mini, fullscreen) reusando o mesmo hook

---

### A3 — `xertica-assistant.tsx` com 1573 linhas — decomposição necessária

**Arquivo:** [`components/assistant/xertica-assistant/xertica-assistant.tsx`](../components/assistant/xertica-assistant/xertica-assistant.tsx)

**Problema:** Mesmo com o `useAssistant` hook extraindo a lógica de estado, o componente ainda tem ~1000 linhas de JSX com sub-componentes definidos inline e seções de renderização muito longas.

**Estrutura atual:**

```
xertica-assistant.tsx (1573 linhas)
├── AssistantTooltipContent (componente inline, linhas 105-131)
├── Tipos exportados (linhas 155-230) ← deveria estar em types.ts
├── XerticaAssistantProps (linhas 246-417) ← 171 linhas de interface!
└── XerticaAssistant (linhas 467-1572)
    ├── Document editor overlay (linhas 598-694)
    ├── Collapsed view (linhas 695-827)
    ├── Chat messages list (linhas 888-1394) ← 506 linhas!
    ├── History/Favorites tabs (linhas 1439-1511)
    ├── Chat input area (linhas 1515-1531)
    └── Feedback dialog (linhas 1539-1570)
```

**Solução — decomposição em sub-componentes:**

```
components/assistant/xertica-assistant/
├── types.ts                          ← NOVO: todos os tipos
├── use-assistant.ts                  ← existente (manter)
├── xertica-assistant.tsx             ← orquestrador (~200 linhas)
└── parts/
    ├── AssistantCollapsedView.tsx    ← NOVO: view colapsada
    ├── AssistantDocumentEditor.tsx   ← NOVO: overlay do editor
    ├── AssistantMessageList.tsx      ← NOVO: lista de mensagens
    ├── AssistantMessage.tsx          ← NOVO: mensagem individual
    ├── AssistantHistoryTab.tsx       ← NOVO: aba de histórico
    ├── AssistantFeedbackDialog.tsx   ← NOVO: dialog de feedback
    └── AssistantTooltipContent.tsx   ← NOVO: tooltip customizado
```

**Impacto:** `xertica-assistant.tsx` passa de 1573 para ~200 linhas. Cada sub-componente é testável e manutenível independentemente.

---

## 🟡 Problemas de Média Prioridade

### M1 — `sidebar.tsx` com 1089 linhas — decomposição parcial necessária

**Arquivo:** [`components/layout/sidebar/sidebar.tsx`](../components/layout/sidebar/sidebar.tsx)

**Problema:** O arquivo já usa compound components (`SidebarRoot`, `SidebarHeader`, `SidebarNav`, `SidebarSearch`, `SidebarFooter`), mas todos estão no mesmo arquivo de 1089 linhas. Além disso, `SidebarTooltipContent` é definido inline (linhas 40-64) — idêntico ao `AssistantTooltipContent` em `xertica-assistant.tsx`.

**Solução:**

```
components/layout/sidebar/
├── sidebar.tsx           ← orquestrador + export público (~100 linhas)
├── use-sidebar.ts        ← existente (manter)
├── SidebarContext.tsx    ← NOVO: contexto interno extraído
└── parts/
    ├── SidebarRoot.tsx   ← NOVO
    ├── SidebarHeader.tsx ← NOVO
    ├── SidebarNav.tsx    ← NOVO
    ├── SidebarSearch.tsx ← NOVO
    └── SidebarFooter.tsx ← NOVO
```

**Oportunidade adicional:** `SidebarTooltipContent` e `AssistantTooltipContent` são idênticos — extrair para componente compartilhado:

```
components/shared/
└── CustomTooltipContent.tsx  ← NOVO: tooltip com portal e arrow
```

---

### M2 — Atalhos de teclado e cookies misturados no `LayoutContext`

**Arquivo:** [`contexts/LayoutContext.tsx`](../contexts/LayoutContext.tsx)

**Problema:** O `LayoutContext` mistura três responsabilidades distintas:

1. **Estado de layout** (sidebar expandida, assistente expandido)
2. **Atalhos de teclado** (`Ctrl+B` para toggle sidebar, linhas 78-86)
3. **Persistência em cookies** (funções `getCookie`/`setCookie`, linhas 25-45)

**Solução — separação de responsabilidades:**

```ts
// contexts/LayoutContext.tsx — apenas estado
export function LayoutProvider({ children }) {
  const [sidebarExpanded, setSidebarExpanded] = usePersistentState('sidebar', true);
  // ...
}

// hooks/use-layout-shortcuts.ts — NOVO: atalhos de teclado
export function useLayoutShortcuts() {
  const { toggleSidebar } = useLayout();
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
        e.preventDefault();
        toggleSidebar();
      }
    };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [toggleSidebar]);
}

// hooks/use-persistent-state.ts — NOVO: estado com persistência em cookie
export function usePersistentState<T>(key: string, defaultValue: T) { ... }
```

---

### M3 — `BrandColorsContext` com utilitário `hexToRgb` inline

**Arquivo:** [`contexts/BrandColorsContext.tsx`](../contexts/BrandColorsContext.tsx) — linhas 79-88

**Problema:** A função `hexToRgb` é um utilitário puro definido dentro do componente Provider. Não pode ser testada isoladamente e não pode ser reutilizada.

**Solução:**

```ts
// utils/color-utils.ts — NOVO
export function hexToRgb(hex: string): { r: number; g: number; b: number } | null { ... }
export function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } { ... }
export function generateColorScale(baseHex: string): Record<string, string> { ... }
```

---

### M4 — `header.tsx` sem hook headless

**Arquivo:** [`components/layout/header/header.tsx`](../components/layout/header/header.tsx)

**Problema:** O `Header` não tem lógica de estado complexa, mas tem lógica de renderização condicional para breadcrumbs, ações e menu do usuário que poderia ser extraída para melhorar a testabilidade.

**Avaliação:** Prioridade baixa — o componente tem 337 linhas mas é principalmente JSX declarativo. Não há estado local significativo. A extração de um `useHeader` hook seria de valor limitado aqui.

**Recomendação:** Manter como está, mas considerar extrair `HeaderUserMenu` e `HeaderBreadcrumbs` como sub-componentes separados para melhorar a legibilidade.

---

## 🟢 Problemas de Baixa Prioridade

### B1 — `SidebarTooltipContent` e `AssistantTooltipContent` são idênticos

**Arquivos:**

- [`components/layout/sidebar/sidebar.tsx`](../components/layout/sidebar/sidebar.tsx) — linhas 40-64
- [`components/assistant/xertica-assistant/xertica-assistant.tsx`](../components/assistant/xertica-assistant/xertica-assistant.tsx) — linhas 105-131

Ambos implementam um tooltip com `TooltipPrimitive.Portal` e `TooltipPrimitive.Arrow`. Extrair para componente compartilhado elimina duplicação.

---

### B2 — `use-sidebar.ts` tem alturas hardcoded

**Arquivo:** [`components/layout/sidebar/use-sidebar.ts`](../components/layout/sidebar/use-sidebar.ts) — linhas 57-91

A função `checkOverflow` usa valores hardcoded (`40px`, `32px`) para calcular overflow de itens de navegação. Esses valores deveriam ser constantes nomeadas ou derivados do DOM.

```ts
// ❌ ATUAL
const itemHeight = 40;
const groupHeaderHeight = 32;

// ✅ MELHOR
const SIDEBAR_ITEM_HEIGHT_PX = 40;
const SIDEBAR_GROUP_HEADER_HEIGHT_PX = 32;
```

---

### B3 — `use-assistant.ts` tem `setTimeout` para simular async ✅ RESOLVIDO

**Arquivo:** [`components/assistant/xertica-assistant/use-assistant.ts`](../components/assistant/xertica-assistant/use-assistant.ts)

**Resolução (v2.1.9):** Todo o dado mock foi extraído para a camada `features/*/data/mock.ts`. A estrutura implementada usa o padrão **swap point**: cada `features/<nome>/data/mock.ts` contém funções `async fetch*()` que simulam latência de rede e retornam dados tipados. Para conectar a uma API real, basta substituir o corpo dessas funções por chamadas `fetch()` ou `axios` — hooks, componentes e tipos permanecem inalterados.

```ts
// features/assistant/data/mock.ts — antes (mock)
export async function fetchAssistantConfig(): Promise<AssistantConfig> {
  await new Promise(resolve => setTimeout(resolve, 150)); // simula latência
  return MOCK_ASSISTANT_CONFIG;
}

// features/assistant/data/mock.ts — depois (API real)
export async function fetchAssistantConfig(): Promise<AssistantConfig> {
  return fetch('/api/assistant/config').then(r => r.json());
}
```

A configuração do assistente (suggestions, richSuggestions, feedbackOptions) é agora consumida via `useAssistantConfig()` (TanStack React Query) com `staleTime: 30min`.

---

## Padrões Positivos — Manter e Expandir

O projeto já tem excelentes exemplos do padrão headless hook que devem servir como referência:

### ✅ `useRichTextEditor` — Exemplo de referência

**Arquivo:** [`components/ui/rich-text-editor/use-rich-text-editor.ts`](../components/ui/rich-text-editor/use-rich-text-editor.ts)

- Interface `UseRichTextEditorProps` bem definida
- Interface `UseRichTextEditorReturn` completa com todos os retornos tipados
- Lógica completamente separada do JSX
- `rich-text-editor.tsx` é JSX puro consumindo o hook

### ✅ `useTreeView` — Exemplo de referência para acessibilidade

**Arquivo:** [`components/ui/tree-view/use-tree-view.ts`](../components/ui/tree-view/use-tree-view.ts)

- WAI-ARIA keyboard navigation implementada no hook
- Componente de UI não precisa conhecer lógica de navegação por teclado

### ✅ `useAssistant` — Boa extração, mas incompleta

**Arquivo:** [`components/assistant/xertica-assistant/use-assistant.ts`](../components/assistant/xertica-assistant/use-assistant.ts)

- Toda a lógica de estado está no hook
- O componente ainda tem 1000+ linhas de JSX que precisam de decomposição

---

## Plano de Implementação Recomendado

### Fase 1 — Correções Críticas (1-2 dias)

| #       | Tarefa                                                        | Arquivo(s)             | Esforço |
| ------- | ------------------------------------------------------------- | ---------------------- | ------- |
| ~~1.1~~ | ~~Corrigir `ThemeToggle` para usar `useTheme()`~~             | ✅ Resolvido em v2.2.0 | —       |
| 1.2     | Criar `types.ts` compartilhado para o assistente              | `types.ts` (novo)      | 1h      |
| 1.3     | Atualizar imports em `AssistenteContext` e `use-assistant.ts` | 2 arquivos             | 30min   |

### Fase 2 — Eliminação de Duplicações (2-3 dias)

| #   | Tarefa                                         | Arquivo(s)     | Esforço |
| --- | ---------------------------------------------- | -------------- | ------- |
| 2.1 | Substituir detecção mobile por `useIsMobile()` | 4 arquivos     | 2h      |
| 2.2 | Extrair `CustomTooltipContent` compartilhado   | 1 arquivo novo | 1h      |
| 2.3 | Mover `hexToRgb` para `utils/color-utils.ts`   | 2 arquivos     | 30min   |

### Fase 3 — Extração de Hooks (3-5 dias)

| #   | Tarefa                                       | Arquivo(s)        | Esforço |
| --- | -------------------------------------------- | ----------------- | ------- |
| 3.1 | Criar `use-audio-player.ts`                  | 1 arquivo novo    | 4h      |
| 3.2 | Refatorar `AudioPlayer.tsx` para usar o hook | `AudioPlayer.tsx` | 2h      |
| 3.3 | Extrair `use-layout-shortcuts.ts`            | 1 arquivo novo    | 1h      |
| 3.4 | Extrair `use-persistent-state.ts`            | 1 arquivo novo    | 1h      |

### Fase 4 — Decomposição de Componentes Grandes (5-8 dias)

| #   | Tarefa                                              | Arquivo(s)       | Esforço |
| --- | --------------------------------------------------- | ---------------- | ------- |
| 4.1 | Decompor `xertica-assistant.tsx` em sub-componentes | 7 arquivos novos | 1 dia   |
| 4.2 | Decompor `sidebar.tsx` em sub-componentes           | 5 arquivos novos | 1 dia   |
| 4.3 | Extrair `HeaderUserMenu` e `HeaderBreadcrumbs`      | 2 arquivos novos | 3h      |

---

## Métricas de Melhoria Esperadas

| Métrica                           | Atual                   | Após Fase 1-2 | Após Fase 3-4 |
| --------------------------------- | ----------------------- | ------------- | ------------- |
| Maior arquivo (linhas)            | 1573                    | 1573          | ~200          |
| Duplicações de detecção mobile    | 4                       | 1             | 1             |
| Definições de tipo duplicadas     | 2 conjuntos             | 1 conjunto    | 1 conjunto    |
| Componentes sem hook headless     | 2 (AudioPlayer, Header) | 2             | 0-1           |
| Risco de dessincronização de tema | Eliminado (v2.2.0)      | Eliminado     | Eliminado     |

---

## Referências

- [Padrão Headless Hook — Kent C. Dodds](https://kentcdodds.com/blog/inversion-of-control)
- [Compound Components Pattern](https://kentcdodds.com/blog/compound-components-with-react-hooks)
- [`docs/architecture.md`](./architecture.md) — Arquitetura atual do projeto
- [`docs/doc-audit.md`](./doc-audit.md) — Auditoria de documentação
