feat(B131): sidebar footer role label + logout icon

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-02 19:17:43 +00:00
parent 0400d0fdc5
commit b6f16557ce
No known key found for this signature in database
2 changed files with 93 additions and 23 deletions

View file

@ -0,0 +1,64 @@
# Impl: Sidebar — papel no perfil + Sair como ícone
Status: aprovado
Atualizado em: 2026-08-02
Issue: #272
Intenção: docs/plans/sidebar-perfil-sair-icone.md
Appetite restante: ~0,5 dia eng (dentro)
## Leitura da intenção
- **Outcome:** Rodapé da sidebar mostra nome + papel; logout vira ícone à direita na mesma linha — sem botão full-width “Sair”.
- **O que NÃO negociar:** destino `/campanha/perfil`; limpeza client-side no logout; rótulos pt-BR existentes; leader lockdown; não mexer no badge de escopo do topo (B130 é outro item).
- **O que reavaliar:** hipótese de manter `<form>` separado abaixo do link — melhor linha única com flex.
## Abordagem recomendada
```mermaid
flowchart LR
A[SidebarFooter flex row] --> B[Link perfil flex-1]
A --> C[form + icon Button]
B --> D[campaignRoleLabels role]
C --> E[handleLogout inalterado]
```
**Opções consideradas:**
- **A:** Ícone dentro do `<Link>` com stopPropagation — rejeitada (acidental logout / a11y confusa).
- **B:** `DropdownMenu` de conta — rejeitada (fora de escopo / rabbit hole).
- **C (recomendada):** Flex row: link de perfil (`flex-1`) + `<form>` com `Button` ghost `size-11` + `LogOutIcon`; subtítulo = `campaignRoleLabels[user.role]`.
**Recomendação:** C — mínimo diff no dono `CampaignSidebar.tsx`; comportamento de logout preservado.
### Componentes / mudanças
- **`CampaignSidebar.tsx`:** footer em `flex`; subtítulo papel; `LogOutIcon`; botão ícone com `aria-label` dinâmico; `handleLogout` e limpezas intactas.
- **Migration:** sem migration.
- **Access / Consent:** N/A.
- **UI:** Impeccable B — shape (linha única) + craft (ghost icon 44px) + a11y (`aria-label`, `sr-only` não necessário se label no botão).
### Dados → forma
N/A — rótulo de papel já na sessão (`campaignRoleLabels`).
## Fases verificáveis
1. **UI** — footer densificado em `CampaignSidebar.tsx`.
2. **Testes** — unit existente de tokens sidebar; opcional assert de `campaignRoleLabels` no source.
3. **Gates** — `pnpm gate:fast`; `pnpm push`.
## Rabbit holes / Não escopo (engenharia)
- Remover `CampaignScopeBadge` do topo (B130).
- Bottom nav / perfil page / confirmação de logout.
## Riscos e mitigação
- **Toque acidental:** alvos separados (link vs botão) — mitigado por layout flex explícito.
- **Spinner sem texto:** `aria-label` “Saindo…” no submit — mitigado.
## Aceite de engenharia
- [x] Aceite de produto da intenção coberto
- [x] Invariantes AGENTS/engineering-standards
- [x] Testes de domínio: shell unit spec de tokens mantido

View file

@ -4,6 +4,8 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState, type FormEvent, type ReactNode } from 'react'
import { LogOutIcon } from 'lucide-react'
import { logoutCampaign } from '@/app/(campaign)/campanha/actions/auth'
import { CampaignScopeBadge } from '@/components/campaign/shared/CampaignScopeBadge'
import { CampaignUserAvatar } from '@/components/campaign/shared/CampaignUserAvatar'
@ -139,30 +141,34 @@ export const CampaignSidebar = ({ user }: { user: CampaignSidebarUser }) => {
</SidebarContent>
<SidebarFooter className="border-t border-sidebar-border">
<Link
href="/campanha/perfil"
className="flex min-w-0 items-center gap-3 rounded-md px-1 py-2 outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
>
<CampaignUserAvatar name={user.name} avatarUrl={user.avatarUrl} size="sm" />
<div className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate text-sm font-medium text-sidebar-foreground">
{user.name}
</span>
<span className="truncate text-xs text-muted-foreground">Meu perfil</span>
</div>
</Link>
<form onSubmit={handleLogout}>
<Button
type="submit"
variant="outline"
size="sm"
disabled={isLoggingOut}
className="min-h-11 w-full border-sidebar-border bg-sidebar font-medium text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
<div className="flex items-center gap-1">
<Link
href="/campanha/perfil"
className="flex min-w-0 flex-1 items-center gap-3 rounded-md px-1 py-2 outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
>
{isLoggingOut ? <Spinner aria-hidden="true" /> : null}
{isLoggingOut ? 'Saindo…' : 'Sair'}
</Button>
</form>
<CampaignUserAvatar name={user.name} avatarUrl={user.avatarUrl} size="sm" />
<div className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate text-sm font-medium text-sidebar-foreground">
{user.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{campaignRoleLabels[user.role]}
</span>
</div>
</Link>
<form onSubmit={handleLogout} className="shrink-0">
<Button
type="submit"
variant="ghost"
size="icon"
disabled={isLoggingOut}
aria-label={isLoggingOut ? 'Saindo…' : 'Sair'}
className="size-11 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
{isLoggingOut ? <Spinner aria-hidden="true" /> : <LogOutIcon />}
</Button>
</form>
</div>
</SidebarFooter>
</Sidebar>
)