Formação — UI (PR-7)
SSOT da camada de apresentação da seção Formação após a re-arquitetura (PR-1..PR-6.5). Toda mutação respeita ADR-018 — Least Privilege (especialista CRUD, coordenador SELECT, administrador NEGADO).
Mapa de componentes
Especialista (management)
| Componente | Caminho | Responsabilidade |
|---|---|---|
FormacaoTabs | src/components/formacao/formacao-tabs.tsx | Tab raiz Cursos ↔ Trilhas. Monta uma vez (hidden toggle — Lazy Mount Once). |
CursosEspecialista | src/components/formacao/cursos-especialista.tsx | Lista de cursos com CRUD, upload de imagem, mudança de status e acesso ao ModulosManager. |
ModulosManager | src/components/formacao/modulos-manager.tsx | Sheet com CRUD + reorder ↑/↓ por curso. Reorder usa two-phase (ordens negativas temporárias). |
TrilhasManager | src/components/formacao/trilhas-manager.tsx | CRUD global de trilhas + attach/detach de cursos, reorder, ativo/inativo. |
Coordenador (consumo)
| Componente | Caminho | Responsabilidade |
|---|---|---|
CursosGrid | src/components/formacao/coordenador/CursosGrid.tsx | Grid filtrável por busca e trilha. Card vertical com banner-topo, título destaque e ProgressoCursoFooter. Bloqueia clique em em_breve (aria-disabled). |
TrilhasSidebar | src/components/formacao/coordenador/TrilhasSidebar.tsx | Lista trilhas ativas; clique toggle. Botão Limpar só aparece com seleção. |
CursoThumbnail | src/components/formacao/coordenador/CursoThumbnail.tsx | Variantes square (legado, ícone+cor) e banner (aspect 16/9, cor chapada da categoria). Usa signed URL do bucket privado quando imagem_url existe. Não chama o hook quando imagem_url é nulo. |
ProgressoCursoFooter | src/components/formacao/coordenador/ProgressoCursoFooter.tsx | Rodapé reutilizável: barra de progresso (verde bg-curso-verde) + texto X de Y aulas assistidas | duração. Silencioso quando totalAulas === 0. Compartilhado entre CursosGrid e CursoIntroCard. |
CursoDetalhe | src/components/formacao/coordenador/CursoDetalhe.tsx | Layout 2-colunas (lg:grid-cols-[1fr_380px]): esquerda = busca + acordeão de módulos como cards; direita = CursoIntroCard sticky. Em viewports < lg, intro empilha acima. |
CursoIntroVideo | src/components/formacao/coordenador/CursoIntroVideo.tsx | Embed YouTube/Vimeo via src/lib/video-utils.ts. URL desconhecida → null (sem throw). |
CursoIntroCard | src/components/formacao/coordenador/CursoIntroCard.tsx | Card lateral do detalhe que reutiliza o átomo de card: banner topo (vídeo intro embed se válido, senão CursoThumbnail variant="banner") + título "Vídeo de introdução" + descrição + contagem de módulos + ProgressoCursoFooter. |
ModuloAccordionItem | src/components/formacao/coordenador/ModuloAccordionItem.tsx | Card colapsável com profundidade (border rounded-lg bg-card shadow-sm). Header Módulo 0X | Título + contador. Aulas em linhas com Aula 0X | Título à esquerda e duração mono à direita. readOnly em em_breve. |
CursoPlayer | src/components/formacao/coordenador/CursoPlayer.tsx | Player com sidebar module-aware (módulo da aula atual abre por padrão). Fallback explícito para URL inválida. |
Identidade visual (tokens semânticos)
Cores de cursos vivem em src/styles/globals.css (HSL) e são expostas em tailwind.config.ts como colors.curso.*:
bg-curso-azul,bg-curso-roxo,bg-curso-laranja,bg-curso-verde,bg-curso-vermelho,bg-curso-amarelo,bg-curso-cinza,bg-curso-rosa,bg-curso-indigo,bg-curso-teal,bg-curso-ambar,bg-curso-slatetext-curso-titulo— destaque de título de curso (violeta do mock).
src/components/videos/helpers.ts (CATEGORIA_MAP / TIPO_CURSO_MAP) mapeia categoria_visual/tipo_curso para esses tokens. Proibido voltar a bg-blue-500/bg-[#hex] — viola o Core "no hardcoded colors".
Helpers puros
src/components/formacao/coordenador/helpers.ts (todos sem side effects, testados):
totaisCursoCompleto(curso)— agrega contagens e duração.filtrarModulosPorTermo(modulos, termo)— case-insensitive; oculta módulos vazios.encontrarModuloDaAula(curso, aulaId)— suporte ao sidebar module-aware.proximaAula(curso, aulaId)— pronta para auto-avanço futuro.mergeAulaDetalhada(resumo, detalhe)— adapter isolando drift entreAulaResumoeVideoDetalhado.
Hooks
| Hook | Papel | Edge function | queryKey | Invalidações |
|---|---|---|---|---|
useModulos(cursoId) | especialista | especialista-modulos (list) | ['formacao','modulos',cursoId] | mutations create/update/delete/reorder invalidam a mesma key |
useTrilhas() | especialista | especialista-trilhas (list) | ['formacao','trilhas'] | todas as mutations + attach/detach_curso |
useTrilhasCoord() | coordenador | coordenador-videos (list_trilhas) | ['formacao','trilhas-coord'] | read-only |
useCursoCompleto(cursoId) | coordenador | coordenador-videos (list_curso_completo) | ['formacao','curso-completo',cursoId] | read-only; compartilhada entre CursoDetalhe e CursoPlayer |
useCursoImagemUpload() | especialista | especialista-cursos (upload_imagem) | — | invalida ['cursos'] e ['formacao'] |
useCursoImagemSignedUrl(path) | ambos | cursos-imagens-signed-url (sign) | ['cursos-imagens-signed',path], staleTime: 50 min | TTL ≤1h |
Estados visuais
- Anti-Flash (
docs/development/ATOMIC_RENDERING.md): todo container Formação fazif (isLoading && !data) return <Skeleton />. Nunca renderiza grid vazio antes do primeiro fetch. em_breve: badge visual +aria-disabledno card; toda aula propagareadOnly. Não chama mutations.- Fallback de iframe (
CursoIntroVideoeCursoPlayer): URL com plataforma desconhecida renderiza placeholderdata-testid="video-fallback"em vez de<iframe src="">. - Empty states distintos: busca sem resultado vs trilha sem cursos vs sem cursos publicados — cada um com microcopy próprio.
Guardrails ADR-018 na UI
<FeatureGate bypassAdmin={false}>envolve todo controle de management. Admin não vê o tab Formação, nem botões de CRUD.- Coordenador renderiza apenas
CursosGrid/TrilhasSidebar/CursoDetalhe/CursoPlayer. Qualquer chamada aespecialista-*é proibida no client e bloqueada pela edge function (requireRole('especialista')). - Upload de imagem (
useCursoImagemUpload) valida MIME (jpeg|png|webp) e tamanho (≤5 MB) fail-fast antes de tocar storage; o blob nunca fica público (leitura sempre via signed URL).
Cobertura de testes (snapshot PR-7)
bunx vitest run src/components/formacao src/hooks/formacao → 74 testes verdes / 13 arquivos.
- Hooks:
useModulos,useTrilhas,useTrilhasCoord,useCursoImagemUpload. - Especialista:
ModulosManager,TrilhasManager. - Coordenador:
CursosGrid,TrilhasSidebar,CursoThumbnail,CursoDetalhe,CursoIntroVideo,CursoPlayer,helpers.
Referências
- SCHEMA.md — modelo de dados.
- EDGE_FUNCTIONS.md — contratos e validação Zod.
- ROADMAP.md — entregas PR-1..PR-7.
- ADR-003 — payload dual-format (
body.params ?? body). - ADR-018 — Formação Least Privilege.
- ADR-010 — Atomic rendering.