mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
docs(plan): C181–C184 + B203 — acervo (player/preview/cortes), nav e Sollinha
Related #1103, #1104, #1105, #1106, #1107
This commit is contained in:
parent
f066bee92b
commit
17ad33f0f8
7 changed files with 1727 additions and 0 deletions
|
|
@ -0,0 +1,85 @@
|
|||
# Acervo: o player sempre deixa voltar ao YouTube e espera a Câmara gerar o trecho
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1103
|
||||
Priority: P2
|
||||
Impeccable: A — N/A sem nova superfície (acréscimo de um controle simétrico no bloco de saída já desenhado + correção de comportamento do resolver)
|
||||
Design UI: N/A — sem UI nova
|
||||
Appetite: ~0,5–1 dia eng; um outcome verificável: numa fala, quem troca para a Câmara sempre consegue voltar ao YouTube na própria página, e a Câmara gerando o trecho não vira um "falhou" de primeira.
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
O aviso do dono é um defeito de duas cabeças na mesma fala. Primeira: ao clicar em "Assistir na Câmara", o player troca a superfície para o trecho da Câmara — mas **não existe caminho de volta** para o embed do YouTube na própria página; a única saída passa a ser um link externo, e quem preferia o YouTube fica ilhado. Segunda: quando a Câmara responde "GERANDO", o player trata isso como fracasso na primeira resposta e manda o usuário tentar de novo, quando na verdade a Câmara está apenas transcodificando o trecho e ele fica pronto segundos depois — o retry então "funciona", o que confirma que a espera é curta e o problema é o app desistir cedo. Para a assessoria, no meio da montagem de uma peça, isso custa a fala: ou ela perde a fonte que estava usando, ou acha que o arquivo não existe. A correção é pequena e simétrica — dar o mesmo controle nos dois sentidos e deixar o clique do player esperar um orçamento curto antes de anunciar "gerando/falhou".
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (`communicator`, coordenação e candidatura) em `/campanha/comunicacao/acervo/[id]`, na mesa ou no celular, com prazo curto.
|
||||
- **Job principal:** alternar entre a superfície da Câmara e a do YouTube na mesma página, e assistir ao trecho sem ser mandado "tentar de novo" por algo que só estava sendo gerado.
|
||||
- **Fluxo desejado:** abre a fala → assiste na superfície preferida (C178) → clica em "Assistir no YouTube" e volta ao embed no ponto da fala → se pedir o trecho da Câmara e ele ainda estiver sendo gerado, o clique espera alguns segundos e o vídeo toca sozinho; se o orçamento estourar, aí sim vê o estado honesto com "Tentar novamente".
|
||||
- **Anti-goals de produto:** autenticar no YouTube ou trocar por embed novo; inventar estado ("sucesso" sem verificação de mídia); novo player/editor; baixar/espelhar/transcodificar; novo `Consent`/collection/migration; expor o acervo fora de `/campanha`.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- Trocar de superfície **nos dois sentidos** na própria página (Câmara ↔ embed do YouTube), com o ponto da fala preservado ao voltar.
|
||||
- Esperar a geração da Câmara **dentro do clique do player** (bounded), em vez de devolver "gerando" na primeira resposta; se estourar o orçamento, o "Tentar novamente" continua existindo e o estado honesto permanece.
|
||||
- Nenhuma regressão nas saídas, na transcrição/posicionamento e na seleção/corte (C162/C166/C167/C172).
|
||||
- **Guardrails:** nada de autenticar no YouTube nem embed novo; não inventar estado; a superfície que o app controla segue preferida (C178); transcrição/posicionamento e seleção/corte intactos; sem `Consent`/collection/migration; URLs públicas do acervo intocadas; crédito "Fonte: Câmara dos Deputados · CC BY 4.0" mantido.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — é feedback de ação no player, não dado de acervo; nenhum KPI/gráfico novo.
|
||||
- **Decisões desbloqueadas:** a assessoria decide seguir com a peça sem trocar de ferramenta; a coordenação decide, pelo uso, se o YouTube volta a ser superfície padrão algum dia.
|
||||
- **Forma:** _adiada ao plano de implementação_ — restrição de produto: o estado do player é feedback de ação, nunca dado persistido.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- **Duas políticas hoje (a raiz do defeito):** a do player `SPEECH_VOD_PLAYER_POLICY = { statusTimeoutMs: 15000, statusRetries: 1, retryDelayMs: 1000, pollAttempts: 0, pollDelayMs: 1000 }` — com `pollAttempts: 0`, o primeiro `GERANDO` já retorna `{state:'gerando'}`; a do corte `SPEECH_VOD_CUT_POLICY = { statusTimeoutMs: 45000, statusRetries: 1, retryDelayMs: 1000, pollAttempts: 2, pollDelayMs: 5000 }` — essa espera e por isso "funciona".
|
||||
- **Copy pt-BR existente (manter):** bloco de saída "Se o vídeo não abrir aqui, assista por outro caminho:", botões "Abrir no YouTube" (link externo, no ponto) e "Assistir na Câmara"; estado de geração já nomeado "A Câmara está gerando o trecho"; retry "Tentar novamente".
|
||||
- **Copy nova (proposta):** botão "Assistir no YouTube" no mesmo bloco de saída, no ponto da fala.
|
||||
- **Embed do YouTube (contrato a preservar):** `https://www.youtube.com/embed/<id>?playsinline=1&rel=0&start=<segundos>`.
|
||||
- Sem `Consent`/collection/migration; nada persistido no servidor.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/components/campaign/speech/SpeechDetailPlayer.tsx` (setter simétrico de `surface` + botão no bloco de saída, `renderYoutubeExit`), `src/utilities/speech/speechVodResolver.ts` + a rota/action `.../comunicacao/acervo/resolver-vod` (`resolveSpeechVodForActor` hoje usa a política do player), `src/lib/speechShare.ts`.
|
||||
- **Precedente a olhar:** C178 (`docs/plans/acervo-player-youtube-sem-parede-de-signin.md`, #1081) — redesenha a superfície padrão; `docs/plans/acervo-vod-sob-demanda.md` (C162/C169, a política do corte e a verificação de mídia).
|
||||
- **Risco de acoplamento:** mesmo arquivo de C178/C172/C173 — serializar; pins em `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/unit/speechVodResolver.unit.spec.ts` (fixa a política do player) e `tests/e2e/campaignSpeechAcervo.e2e.spec.ts` precisam ser renegociados no item.
|
||||
|
||||
## Dependências
|
||||
|
||||
- **Dura:** C178 — mesmo arquivo `SpeechDetailPlayer.tsx`; a superfície padrão precisa existir antes de dar a volta.
|
||||
- **Suave:** C172/C173 — serializam no mesmo arquivo.
|
||||
- Nenhum bloqueio de dados.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Fazer o YouTube tocar quando ele bloqueia; detectar/classificar a causa do bloqueio.
|
||||
- Baixar/espelhar/transcodificar; tocar a superfície do VOD com um player novo.
|
||||
- Tornar o YouTube a superfície padrão de novo (isso é C178).
|
||||
- Segundo player/editor; exposição pública do acervo; `Consent`/collection/migration.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Consertar o iframe do YouTube".** Se alguém "só completar": IFrame API, eventos, OAuth, player próprio. **Corte neste item:** a volta é um botão na nossa própria página; o YouTube continua embed anônimo.
|
||||
- **Espera infinita / fila de render.** Se alguém "só completar": estado assíncrono persistido, cron, "avise quando ficar pronto". **Corte neste item:** orçamento curto dentro do clique; estourou → estado honesto + retry.
|
||||
- **"Retry resolve, então é só mandar tentar".** **Corte:** isso é o defeito, não a solução — o app espera antes de acusar falha.
|
||||
- **Redesenhar o player inteiro.** **Corte:** encaixe cirúrgico; transcrição, seleção e corte intactos.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Quanto esperar dentro do clique do player?** **Opções:** A) reusar a política do corte (45s/2 polls de 5s) | B) política própria do player (ex.: alguns polls curtos) | C) manter `pollAttempts:0` e só melhorar a copy. **Recomendação:** B — um orçamento curto e próprio do clique, entre o imediato de hoje e o do corte, preservando o "bounded" e a verificação de mídia. _(assumido — validar com produto)_
|
||||
- **O botão "Assistir no YouTube" aparece também no estado "gerando"?** **Opções:** A) sim, sempre que houver embed | B) só quando a superfície for `vod` e a mídia estiver resolvida. **Recomendação:** A — a saída é justamente o que falta hoje; nunca deixar o usuário sem caminho. _(assumido)_
|
||||
- **Onde fica o botão?** **Opções:** A) no bloco de saída já existente | B) junto das ações do player. **Recomendação:** A — simétrico a "Assistir na Câmara", mesmo bloco, mesmo desenho.
|
||||
|
||||
## Referências
|
||||
|
||||
- GitHub Issue: —
|
||||
- Planos irmãos: `docs/plans/acervo-player-youtube-sem-parede-de-signin.md` (C178), `docs/plans/acervo-vod-sob-demanda.md` (C162), `docs/plans/c171-player-youtube-sem-signin.md`
|
||||
- Arquivos-chave (pista, não contrato): `src/components/campaign/speech/SpeechDetailPlayer.tsx`, `src/utilities/speech/speechVodResolver.ts`, `src/lib/speechShare.ts`, `src/app/(campaign)/campanha/actions/speech.ts`, `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/unit/speechVodResolver.unit.spec.ts`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts`
|
||||
- `AGENTS.md` / `AGENTS-campaign.md` — vertical Comunicação, gate do acervo e crédito CC BY
|
||||
|
||||
## Self-score (shaping)
|
||||
|
||||
5/5 — (1) fatia = um outcome verificável (volta simétrica ao YouTube + espera bounded na geração); (2) appetite ~0,5–1 dia comporta o controle simétrico e o ajuste de política, sem schema; (3) persona/job/aceite em linguagem de produto; (4) direção no codebase é hipótese com precedentes e arquivos nomeados; (5) zero decisão dura de engenharia (política, rota e shape do estado ficam para o plano de implementação).
|
||||
93
docs/plans/acervo-preview-frame-do-meio-da-fala.md
Normal file
93
docs/plans/acervo-preview-frame-do-meio-da-fala.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Acervo: o preview de cada fala mostra um frame do meio daquela fala
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1104
|
||||
Priority: P2
|
||||
Impeccable: A — N/A sem nova superfície (muda a origem da imagem num slot já existente; sem mudança de layout/interação)
|
||||
Design UI: N/A — sem UI nova (o artefato de design não representa o frame real — placeholder não é evidência)
|
||||
Appetite: ~1–1,5 dia eng; um outcome verificável: cada resultado da busca/lista do acervo mostra uma imagem da PRÓPRIA fala (não a capa da sessão inteira), e falas da mesma sessão deixam de repetir a mesma imagem.
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
Relato do usuário (verbatim): _"Os previews de cada fala na página de busca estão mostrando sempre o mesmo frame do Youtube. Quando na verdade eu queria um frame do meio da fala de Jorge Solla. O frame do youtube geralmente mostra uma pessoa aleatória, pois é o início da sessão inteira e não da fala de Jorge Solla."_
|
||||
|
||||
Hoje o preview de cada fala na busca do acervo (`/campanha/comunicacao/acervo`) é sempre a capa do vídeo do YouTube — o primeiro frame da sessão inteira. Como toda fala de uma sessão aponta para o mesmo vídeo, todos os resultados daquela sessão exibem a MESMA imagem, e essa imagem costuma mostrar outra pessoa, não Jorge Solla. A assessoria varre a lista e não reconhece nada: a miniatura não ajuda a distinguir uma fala da outra e ainda obriga a abrir fala por fala para descobrir qual é qual. O que se quer é uma âncora visual por fala: um frame do meio daquela fala.
|
||||
|
||||
## Reabertura de anti-goal (evidência de produto)
|
||||
|
||||
C175/#1085 (`docs/plans/acervo-preview-do-trecho-na-listagem.md` + `...-impl.md`) entregou a miniatura `hqdefault` do YouTube e declarou EXPLICITAMENTE como anti-goal extrair/renderizar um frame no servidor ou usar ffmpeg — "meio do trecho" foi dado como impossível. A evidência de produto acima mostra que o anti-goal produziu um preview inútil (repetido por sessão e mostrando o começo da sessão). Este item **reabre aquele anti-goal**, conforme `AGENTS.md` ("reopen only with new product evidence"). O que muda: agora há um pedido de produto explícito e já existe no repo um caminho de extração de vídeo (o pipeline ffmpeg do corte de trecho) que torna o frame do meio viável para falas com VOD da Câmara.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (jornalista/videomaker), com prazo curto, varrendo a busca do acervo no escritório ou em campo.
|
||||
- **Job principal:** reconhecer cada fala pela imagem e decidir qual abrir, sem abrir fala por fala.
|
||||
- **Fluxo desejado:** busca/filtra → cada resultado mostra uma imagem daquela fala → bate o olho e reconhece a cena → abre só a fala certa.
|
||||
- **Anti-goals de produto:** LLM ou curadoria manual de frames; expor dado novo; mexer no leader/advisor (seguem fora do acervo); mirroring/backfill de todas as capas; o valor é por-fala, nunca por-sessão; player/hover-play na lista.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- Cada fala com VOD da Câmara mostra um frame do meio da fala (não a capa da sessão).
|
||||
- Falas da mesma sessão deixam de compartilhar a mesma imagem.
|
||||
- Quando não há VOD, mantém-se a capa atual do YouTube (fallback honesto) ou um slot neutro — nunca imagem quebrada.
|
||||
- **Guardrail (performance):** miniatura pequena, `lazy`, sem CLS, nunca bloqueia a listagem.
|
||||
- **Guardrail (LGPD/escopo):** nada novo de Consent; nada exposto além do que a fala já expõe na lista.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — não é métrica nem agregado; é uma escolha de origem de imagem (reconhecimento visual). Nenhum número nasce aqui.
|
||||
- **Decisões desbloqueadas:** a assessoria escolhe qual fala abrir pelo reconhecimento da cena.
|
||||
- **Forma:** _adiada ao plano de implementação_ — aqui só a restrição de produto: nada de dashboard/contagem nova.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- **O que existe hoje:** `youtubeThumbnailUrl(videoId)` → `https://i.ytimg.com/vi/<videoId>/hqdefault.jpg` (`src/lib/speechVod.ts:168-169`), consumido em `src/utilities/speech/speechViewModels.ts:240` (`SpeechListItemViewModel.thumbnailUrl`) e renderizado por `SpeechResultThumbnail.tsx` (`h-20 w-32`) em `SpeechResultCard.tsx:175-181`.
|
||||
- **A API do VOD não traz poster:** `parseVodStatus` (`src/lib/speechVod.ts:72-92`) mapeia só `titulo/subtitulo/duracao/horario/linkParaDownload/linkParaReproducao`.
|
||||
- **Campos da fala disponíveis hoje** (`src/collections/Speech.ts`): `durationSeconds:155`, `eventStartAt:218`, `youtubeUrl:230`, `excerptTMs:252`, `vodPlaybackUrl:259`, `vodDownloadUrl:265`; `excerptTMs` é o offset de início da fala na sessão.
|
||||
- **Instante que define "meio da fala":** `excerptTMs + durationSeconds/2` (início da fala + metade da duração). Este é o único literal que a intenção fixa; o resto (onde/como gerar, cache) é implementação.
|
||||
- **Sem campo de poster:** `src/collections/Media.ts:37-39` só tem `alt`; nenhum cache de poster de fala existe.
|
||||
- **Pipeline de vídeo já existente:** `src/utilities/speech/speechCutJob.ts:230-245` baixa o VOD para `source.mp4` e roda ffmpeg; `buildSpeechCutFfmpegArgs` (`src/lib/speechCut.ts:157-195`) só corta `[start,end]`, sem extrair still.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/utilities/speech/speechViewModels.ts` (campo da miniatura), `SpeechResultThumbnail.tsx`/`SpeechResultCard.tsx` (slot já existe), `src/utilities/speech/speechVodResolver.ts`/`speechCutJob.ts`/`src/lib/speechCut.ts` (ffmpeg), campos de `src/collections/Speech.ts`.
|
||||
- **Precedente a olhar:** `docs/plans/acervo-preview-do-trecho-na-listagem-impl.md` (o VM já é dono do link da miniatura; raw `<img>`; e2e pina o literal).
|
||||
- **Risco de acoplamento:** não transformar isto no chore DRY C179/#1093 (helper único da URL `hqdefault`); não alterar busca/excerto/highlight; leader lockdown intocado; os testes que hoje pinam `hqdefault` precisarão de revisão no plano de implementação.
|
||||
|
||||
## Dependências
|
||||
|
||||
- Nenhuma dura. Soft: C172 (mesma matemática de coordenadas da fala) e C178 (mesma vertical do acervo). C179 é só DRY do helper de thumbnail.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Player, autoplay ou hover-play na lista.
|
||||
- Mirroring/backfill de todas as capas em S3/bucket.
|
||||
- Extração de frame para falas sem VOD.
|
||||
- Trocar `next/image` (o e2e browserless não alcança `i.ytimg.com`).
|
||||
- Consolidar o helper `hqdefault` nas 3 ocorrências existentes (destino: C179/#1093).
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Extrair frame de todas as falas / backfill geral."** Se alguém "só completar": job de mirroring, storage, reconciliação e custo. **Corte neste item:** extração sob demanda, só onde há VOD.
|
||||
- **"Escolher o frame à mão / curadoria manual."** Se alguém "só completar": fila editorial e UI de seleção. **Corte neste item:** o instante é `excerptTMs + durationSeconds/2`, sem curadoria.
|
||||
- **"Preview em vídeo no hover."** Se alguém "só completar": embed, autoplay, banda e ruído na varredura. **Corte neste item:** imagem estática.
|
||||
- **"Consertar os 3 hotlinks de `hqdefault` de uma vez."** Se alguém "só completar": blast radius em `corte/[id]` e no player e quebra do pino e2e. **Corte neste item:** fica no C179.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **De onde vem o frame do meio?** **Opções:** A) extrair do MP4 do VOD da Câmara no instante `excerptTMs + durationSeconds/2`, reusando o pipeline ffmpeg do corte, gerado sob demanda e cacheado | B) storyboard do YouTube na janela da fala (frágil/não documentado) | C) manter `hqdefault` como está. **Recomendação:** A quando há VOD — é o único caminho que entrega o meio da fala real; B rejeitada salvo prova de que funciona; C rejeitada — é exatamente o defeito. _(assumido — validar com produto)_
|
||||
- **E quando não há VOD?** **Opções:** A) capa atual do YouTube como fallback honesto | B) slot neutro. **Recomendação:** A — mantém reconhecimento parcial sem mentir sobre a origem; usar B só se o fallback confundir (mesma imagem repetida por sessão).
|
||||
- **O frame é cacheado ou recalculado a cada render?** **Opções:** A) gerar uma vez e reusar | B) recalcular sob demanda. **Recomendação:** A — miniatura pequena e estável, sem custo por varredura. _(a forma concreta fica na implementação)_
|
||||
- **Comportamento quando o frame extraído vier ruim/escuro?** **Opções:** A) aceitar e mostrar | B) cair para a capa do YouTube. **Recomendação:** A — não inventar heurística de qualidade neste item; reavaliar com uso real.
|
||||
|
||||
## Referências
|
||||
|
||||
- `docs/plans/acervo-preview-do-trecho-na-listagem.md` (C175 — anti-goal reaberto) e `docs/plans/acervo-preview-do-trecho-na-listagem-impl.md`
|
||||
- `docs/plans/c167-cortar-trecho-publicar.md` (precedente do pipeline de corte)
|
||||
- `src/utilities/speech/speechCutJob.ts` · `src/lib/speechCut.ts` · `src/lib/speechVod.ts` · `src/utilities/speech/speechViewModels.ts`
|
||||
- `AGENTS.md` / `AGENTS-campaign.md` — convenções do acervo e da campanha
|
||||
|
||||
## Self-score (shaping)
|
||||
|
||||
5/5 — (1) fatia = um outcome verificável (cada fala com uma imagem da própria fala, sem repetição por sessão); (2) appetite ~1–1,5 dia, aditivo ao slot existente; (3) persona + job + aceite claros, com fallback honesto declarado; (4) direção no codebase é hipótese, sem schema/signature; (5) zero decisão dura de engenharia — só o literal do instante "meio da fala" e a reabertura de anti-goal explicitada.
|
||||
690
docs/plans/biblioteca-cortes-apagar-e-falhas-ui-design.html
Normal file
690
docs/plans/biblioteca-cortes-apagar-e-falhas-ui-design.html
Normal file
|
|
@ -0,0 +1,690 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR" data-theme="campaign">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>Design — Biblioteca de cortes: apagar e recuperar falhas</title>
|
||||
<style type="text/tailwindcss">
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme='campaign'] {
|
||||
--background: #ffffff;
|
||||
--foreground: #1c1917;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1c1917;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1c1917;
|
||||
--primary: #c51414;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f5f5f4;
|
||||
--secondary-foreground: #292524;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #6b7280;
|
||||
--accent: #f4f4f5;
|
||||
--accent-foreground: #1c1917;
|
||||
--destructive: #b42318;
|
||||
--border: rgb(28 25 23 / 12%);
|
||||
--input: rgb(28 25 23 / 16%);
|
||||
--ring: #c51414;
|
||||
--sidebar: #fafaf9;
|
||||
--sidebar-foreground: #1c1917;
|
||||
--sidebar-accent: #f5f5f4;
|
||||
--sidebar-border: rgb(28 25 23 / 12%);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.scene-shadow {
|
||||
box-shadow: 0 18px 48px rgb(28 25 23 / 10%);
|
||||
}
|
||||
.dialog-shadow {
|
||||
box-shadow: 0 24px 80px rgb(28 25 23 / 25%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-stone-100 text-foreground antialiased">
|
||||
<!-- Ícones abaixo seguem o grid/stroke do Lucide; não há asset próprio. -->
|
||||
<main class="mx-auto max-w-[1400px] space-y-12 px-5 py-10 md:px-8 md:py-14">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-medium text-primary">Design de gate · campanha</p>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight">
|
||||
Biblioteca limpa, falha recuperável
|
||||
</h1>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
A assessoria apaga um corte sem serventia com confirmação proporcional ao risco e tenta
|
||||
novamente uma falha no mesmo lugar em que a encontra.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- CENA A — desktop / lista -->
|
||||
<section aria-labelledby="scene-a-title">
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-muted-foreground">Cena A · desktop ~1280</p>
|
||||
<h2 id="scene-a-title" class="mt-1 text-base font-semibold">
|
||||
Lista com os três estados
|
||||
</h2>
|
||||
</div>
|
||||
<p class="hidden text-xs text-muted-foreground md:block">
|
||||
Ações rotineiras à esquerda; apagar isolado à direita.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="scene-shadow mx-auto h-[790px] max-w-[1280px] overflow-hidden rounded-xl border border-border bg-background"
|
||||
>
|
||||
<div class="flex h-full">
|
||||
<aside
|
||||
class="hidden w-64 shrink-0 border-r border-sidebar-border bg-sidebar p-4 lg:flex lg:flex-col"
|
||||
>
|
||||
<div class="flex h-12 items-center gap-3 border-b border-sidebar-border pb-4">
|
||||
<div
|
||||
class="grid size-8 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground"
|
||||
>
|
||||
S
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Campanha</p>
|
||||
<p class="text-xs text-muted-foreground">Mesa de trabalho</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="mt-4 space-y-1 text-sm" aria-label="Navegação ilustrativa">
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 9-8 9 8" />
|
||||
<path d="M5 10v10h14V10" />
|
||||
</svg>
|
||||
Início
|
||||
</a>
|
||||
<a
|
||||
href="/campanha/comunicacao"
|
||||
class="flex h-8 items-center gap-3 rounded-md bg-sidebar-accent px-2 font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3Z" />
|
||||
<path d="M11.6 16.9 13 22H7l-1.3-7" />
|
||||
</svg>
|
||||
Comunicação
|
||||
</a>
|
||||
<div class="mx-3.5 border-l border-sidebar-border px-2.5 py-0.5">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo"
|
||||
class="flex h-7 items-center rounded-md px-2 text-muted-foreground"
|
||||
>Acervo de falas</a
|
||||
>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes"
|
||||
class="flex h-7 items-center rounded-md bg-card px-2 font-medium text-primary ring-1 ring-border"
|
||||
>Biblioteca de cortes</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
class="mt-auto border-t border-sidebar-border pt-4 text-xs text-muted-foreground"
|
||||
>
|
||||
Assessoria de comunicação
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<header class="flex h-16 items-center justify-between border-b border-border px-6">
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Comunicação</p>
|
||||
<p class="text-sm font-medium">Biblioteca de cortes</p>
|
||||
</div>
|
||||
<div class="size-8 rounded-full bg-secondary ring-1 ring-border"></div>
|
||||
</header>
|
||||
|
||||
<div class="mx-auto max-w-4xl p-6 lg:p-8">
|
||||
<div class="mb-8">
|
||||
<label for="desktop-search" class="sr-only"
|
||||
>Buscar corte por título ou descrição</label
|
||||
>
|
||||
<div class="relative max-w-xl">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<input
|
||||
id="desktop-search"
|
||||
readonly
|
||||
placeholder="Buscar por título ou descrição…"
|
||||
class="h-10 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Publicado -->
|
||||
<article class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span
|
||||
class="inline-flex h-5 items-center rounded-full bg-primary px-2 text-xs font-normal text-primary-foreground"
|
||||
>Publicado</span
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">Criado em 16 set. 2026</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium">Título do corte publicado</h3>
|
||||
<div
|
||||
class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
|
||||
>
|
||||
<a href="#" class="underline underline-offset-4 hover:text-foreground"
|
||||
>Fala de origem</a
|
||||
><span aria-hidden="true">·</span><span>00:42</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes/101"
|
||||
class="inline-flex min-h-10 items-center gap-2 rounded-lg border border-border bg-background px-3 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</svg>
|
||||
Abrir corte
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-h-10 items-center gap-2 rounded-lg px-3 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
Copiar link
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto inline-flex min-h-10 items-center rounded-lg px-3 text-xs font-medium text-destructive hover:bg-red-50"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Despublicado -->
|
||||
<article class="rounded-xl border border-dashed border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span
|
||||
class="inline-flex h-5 items-center rounded-full border border-border px-2 text-xs font-normal"
|
||||
>Despublicado</span
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">Criado em 15 set. 2026</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium">Título do corte despublicado</h3>
|
||||
<div
|
||||
class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
|
||||
>
|
||||
<a href="#" class="underline underline-offset-4 hover:text-foreground"
|
||||
>Fala de origem</a
|
||||
><span aria-hidden="true">·</span><span>01:08</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes/102"
|
||||
class="inline-flex min-h-10 items-center gap-2 rounded-lg border border-border bg-background px-3 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</svg>
|
||||
Abrir corte
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto inline-flex min-h-10 items-center rounded-lg px-3 text-xs font-medium text-destructive hover:bg-red-50"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Falhou -->
|
||||
<article class="rounded-xl border border-dashed border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span
|
||||
class="inline-flex h-5 items-center rounded-full bg-destructive px-2 text-xs font-normal text-white"
|
||||
>Falhou</span
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">Criado em 15 set. 2026</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium">Título do corte que não foi concluído</h3>
|
||||
<div
|
||||
class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground"
|
||||
>
|
||||
<a href="#" class="underline underline-offset-4 hover:text-foreground"
|
||||
>Fala de origem</a
|
||||
><span aria-hidden="true">·</span><span>00:35</span>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-h-10 items-center gap-2 rounded-lg bg-primary px-3 text-xs font-medium text-primary-foreground hover:opacity-90"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 6v6h-6" />
|
||||
<path d="M20 12a8 8 0 1 0-2.34 5.66" />
|
||||
</svg>
|
||||
Tentar novamente
|
||||
</button>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes/103"
|
||||
class="inline-flex min-h-10 items-center gap-2 rounded-lg border border-border bg-background px-3 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</svg>
|
||||
Abrir corte
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto inline-flex min-h-10 items-center rounded-lg px-3 text-xs font-medium text-destructive hover:bg-red-50"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENAS B e C — diálogos sempre visíveis para o gate -->
|
||||
<section aria-labelledby="dialog-scenes-title">
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-medium text-muted-foreground">Cenas B e C · confirmação</p>
|
||||
<h2 id="dialog-scenes-title" class="mt-1 text-base font-semibold">
|
||||
O aviso cresce somente quando existe link público
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<div
|
||||
class="relative flex min-h-[410px] items-center justify-center overflow-hidden rounded-xl border border-border bg-stone-200 p-5"
|
||||
>
|
||||
<div class="absolute inset-0 bg-foreground/35"></div>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="published-dialog-title"
|
||||
class="dialog-shadow relative w-full max-w-md rounded-xl border border-border bg-popover p-6 text-popover-foreground"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="grid size-9 shrink-0 place-items-center rounded-full bg-red-50 text-destructive"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="published-dialog-title" class="text-base font-semibold">
|
||||
Apagar este corte?
|
||||
</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
O link público
|
||||
<span class="font-medium text-foreground">/corte/<id></span> deixa de
|
||||
funcionar para quem já recebeu. Esta ação não pode ser desfeita.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 items-center justify-center rounded-lg border border-border bg-background px-4 text-sm font-medium"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 items-center justify-center rounded-lg bg-destructive px-4 text-sm font-medium text-white"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="absolute left-4 top-4 rounded-full bg-white/90 px-2.5 py-1 text-xs font-medium text-foreground"
|
||||
>Publicado</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex min-h-[410px] items-center justify-center overflow-hidden rounded-xl border border-border bg-stone-200 p-5"
|
||||
>
|
||||
<div class="absolute inset-0 bg-foreground/35"></div>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="failed-dialog-title"
|
||||
class="dialog-shadow relative w-full max-w-md rounded-xl border border-border bg-popover p-6 text-popover-foreground"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="grid size-9 shrink-0 place-items-center rounded-full bg-red-50 text-destructive"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 16h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="failed-dialog-title" class="text-base font-semibold">
|
||||
Apagar este corte?
|
||||
</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
Esta ação não pode ser desfeita.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 items-center justify-center rounded-lg border border-border bg-background px-4 text-sm font-medium"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 items-center justify-center rounded-lg bg-destructive px-4 text-sm font-medium text-white"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="absolute left-4 top-4 rounded-full bg-white/90 px-2.5 py-1 text-xs font-medium text-foreground"
|
||||
>Despublicado ou falhou</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA D — mobile -->
|
||||
<section aria-labelledby="scene-d-title">
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-medium text-muted-foreground">Cena D · mobile ~390</p>
|
||||
<h2 id="scene-d-title" class="mt-1 text-base font-semibold">
|
||||
Falha e próxima ação cabem sem esconder apagar
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="scene-shadow mx-auto w-full max-w-[390px] overflow-hidden rounded-[28px] border-[6px] border-stone-800 bg-background"
|
||||
>
|
||||
<div
|
||||
class="flex h-14 items-center justify-between bg-primary px-4 text-primary-foreground"
|
||||
>
|
||||
<button aria-label="Abrir menu" class="grid size-10 place-items-center rounded-lg">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-sm font-semibold">Biblioteca de cortes</span>
|
||||
<div class="size-10"></div>
|
||||
</div>
|
||||
<div class="px-4 py-5">
|
||||
<label for="mobile-search" class="sr-only">Buscar corte por título ou descrição</label>
|
||||
<div class="relative">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<input
|
||||
id="mobile-search"
|
||||
readonly
|
||||
placeholder="Buscar cortes…"
|
||||
class="h-11 w-full rounded-md border border-input bg-background pl-9 pr-3 text-base outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-3">
|
||||
<article class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-flex h-5 items-center rounded-full bg-primary px-2 text-xs text-primary-foreground"
|
||||
>Publicado</span
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">16 set. 2026</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium">Título do corte publicado</h3>
|
||||
<p class="mt-1.5 text-xs text-muted-foreground">
|
||||
<span class="underline underline-offset-4">Fala de origem</span> · 00:42
|
||||
</p>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes/101"
|
||||
class="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-border px-2 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" /></svg
|
||||
>Abrir corte
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-2 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg
|
||||
>Copiar link
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="col-span-2 min-h-11 rounded-lg text-xs font-medium text-destructive hover:bg-red-50"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="rounded-xl border border-dashed border-border bg-card p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-flex h-5 items-center rounded-full bg-destructive px-2 text-xs text-white"
|
||||
>Falhou</span
|
||||
>
|
||||
<span class="text-xs text-muted-foreground">15 set. 2026</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium">Título do corte que não foi concluído</h3>
|
||||
<p class="mt-1.5 text-xs text-muted-foreground">
|
||||
<span class="underline underline-offset-4">Fala de origem</span> · 00:35
|
||||
</p>
|
||||
<div class="mt-3 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 6v6h-6" />
|
||||
<path d="M20 12a8 8 0 1 0-2.34 5.66" />
|
||||
</svg>
|
||||
Tentar novamente
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes/103"
|
||||
class="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-border px-2 text-xs font-medium"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="6 3 20 12 6 21 6 3" /></svg
|
||||
>Abrir corte
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-11 rounded-lg text-xs font-medium text-destructive hover:bg-red-50"
|
||||
>
|
||||
Apagar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
aria-label="Navegação inferior ilustrativa"
|
||||
class="grid h-16 grid-cols-5 border-t border-border bg-background px-2 text-[10px] text-muted-foreground"
|
||||
>
|
||||
<span class="grid place-items-center">Início</span
|
||||
><span class="grid place-items-center">Municípios</span
|
||||
><span class="grid place-items-center">Atualizações</span
|
||||
><span class="grid place-items-center">Agenda</span
|
||||
><span class="grid place-items-center text-primary">Mais</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
105
docs/plans/biblioteca-cortes-apagar-e-falhas.md
Normal file
105
docs/plans/biblioteca-cortes-apagar-e-falhas.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Biblioteca de cortes: apagar um corte e não acumular cortes falhados
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1105
|
||||
Priority: P2
|
||||
Impeccable: B — encaixe na biblioteca de cortes (lista + detalhe): ação de apagar e tratamento de corte falhado
|
||||
Design UI: docs/plans/biblioteca-cortes-apagar-e-falhas-ui-design.html
|
||||
Appetite: ~1 dia eng; um outcome verificável: a assessoria consegue apagar um corte que não serve, e a lista não acumula itens "Falhou" sem ação.
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
A biblioteca de cortes (C168) resolveu o reencontro, mas deixou dois becos. Primeiro: um corte errado, duplicado ou que ninguém vai usar fica lá para sempre — só um admin do Payload consegue apagar. A assessoria, que é quem sabe que o item não presta, não tem como limpar a própria biblioteca. Segundo: um corte que falhou continua listado como um item "Falhou" morto, sem nenhuma ação; o único "Tentar novamente" que existe mora no diálogo da fala de origem, não onde a pessoa vê a falha. O pedido é direto: poder apagar um corte, e não acumular cortes falhados — ou eles somem sozinhos, ou aparecem com um "Tentar novamente" acionável.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (`communicator`) e coordenação/candidatura, em `/campanha/comunicacao/acervo/cortes`, na mesa ou no celular, sob prazo — quem não é da vertical (`advisor`, `leader`) segue negado.
|
||||
- **Job principal:** "manter a biblioteca limpa e recuperar um corte que falhou sem ter de voltar à fala de origem".
|
||||
- **Fluxo desejado:** abre a biblioteca → vê um corte que não serve (ou um "Falhou") → no item ou no detalhe decide apagar (com confirmação) ou tentar de novo → a lista reflete: o corte sumiu, ou o corte falhado voltou a processar.
|
||||
- **Anti-goals de produto:** não virar lixeira/desfazer (soft delete), restauração de apagado, ações em lote, editor de vídeo, métricas ou segundo fluxo de corte.
|
||||
- **Reabertura de anti-goal (produto):** o C168 (#1015, `docs/plans/c168-biblioteca-cortes.md`) declarou "deletar corte não existe na UI (admin pode)" como guardrail aceito. O pedido humano reabre esse anti-goal de propósito. Ao apagar, respeitar o contrato da página pública: um corte `published` tem link público `/corte/<id>`, então apagá-lo é ação explícita e avisada — nunca em silêncio.
|
||||
|
||||
### Esboço de fluxo (B)
|
||||
|
||||
```text
|
||||
[biblioteca de cortes: lista] → abre item/corte
|
||||
→ corte que não serve: [Apagar] → confirmação (avisa o link público se published) → some da lista
|
||||
→ corte "Falhou": [Tentar novamente] (ou já não aparece como item morto)
|
||||
→ [outcome: biblioteca limpa e corte falhado recuperável de onde se vê a falha]
|
||||
```
|
||||
|
||||
### Design UI (B)
|
||||
|
||||
- Design UI (gate): `docs/plans/biblioteca-cortes-apagar-e-falhas-ui-design.html`
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- A biblioteca (lista e/ou detalhe) permite apagar um corte, com confirmação explícita do efeito no link público quando o corte está publicado.
|
||||
- Corte falhado não fica como item morto: ou é descartado automaticamente, ou aparece com "Tentar novamente" acionável — nunca as duas coisas ausentes.
|
||||
- Nenhuma leitura nova de métricas, comentários ou coleções.
|
||||
- **Quem pode apagar:** a mesma audiência da vertical que hoje lê/edita o acervo (`communicator`, `coordinator`, `candidate`), alinhada ao kill-switch de publicar/despublicar; o admin do Payload segue podendo. `advisor` e `leader` negados (fail-closed).
|
||||
- **Guardrail de produto preservado:** apagar um `failed` não quebra nada público (nunca houve página); apagar um `published` quebra o `/corte/<id>` já distribuído — por isso o aviso.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — é uma decisão de estado do item (apagar/recuperar), não um painel.
|
||||
- **Decisões desbloqueadas:** a assessoria decide o que sai da biblioteca e o que vale tentar de novo; nenhuma leitura agregada nova.
|
||||
- **Forma:** _adiada ao plano de implementação_ — restrição de produto: sem contadores de biblioteca nem métricas de falha.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- **Vocabulário de status:** `processing` | `published` | `unpublished` | `failed`; o rótulo visível de `failed` é "Falhou".
|
||||
- **Contrato público:** `failed` **nunca** tem página pública (nada foi publicado); `published` tem link público `/corte/<id>` — apagar quebra esse link para quem já recebeu.
|
||||
- **Rotas internas:** `/campanha/comunicacao/acervo/cortes` (lista) e `/campanha/comunicacao/acervo/cortes/[id]` (detalhe).
|
||||
- **Retry existente:** é por request (`retryOf`) e só aceita `status === 'failed'` — reusa a row, não duplica.
|
||||
- **Copy de confirmação (apagar publicado):** título "Apagar este corte?" · corpo "O link público /corte/<id> deixa de funcionar para quem já recebeu. Esta ação não pode ser desfeita." · botões "Cancelar" / "Apagar".
|
||||
- **Copy de confirmação (apagar não publicado/falho):** mesma pergunta sem a linha do link público; botões "Cancelar" / "Apagar".
|
||||
- Acesso: `communicator`, `coordinator`, `candidate` apagam e reprocessam; `advisor` e `leader` negados (fail-closed); admin mantém o delete do Payload.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/collections/SpeechCut.ts` (access de delete hoje é só-admin), `src/utilities/speech/speechCutListUrl.ts` + `speechCutPageData.ts` (o `where` sem `q` volta `{}` e a lista inclui `failed`), `src/components/campaign/speech/SpeechCutLibraryCard.tsx` e o detalhe `.../cortes/[id]/page.tsx`, `SpeechCutDialog.tsx` + `actions/speech.ts` (retry reusável), e as rotas `cortes/[id]/...`.
|
||||
- **Precedente a olhar:** o kill-switch de publicar/despublicar e a confirmação de "Despublicar este corte?" do C168; o retry por `retryOf` do C167.
|
||||
- **Risco de acoplamento:** não criar fluxo paralelo ao retry do C167 (editar o dono); não misturar com o `where`/ordenação das falas; gate `speechCatalog` e leader lockdown intocados; `/corte/<id>` é do C167 (aqui só se decide o efeito de apagar).
|
||||
|
||||
## Dependências
|
||||
|
||||
- **C168** (#1015, entregue) — a biblioteca é a superfície; seu anti-goal de "não deletar pela UI" é reaberto aqui.
|
||||
- **C169** (#1045, entregue) — falha honesta e retry por row; é o material do tratamento de corte falhado.
|
||||
- Suave: **C167** (#1014) — o retry por request que se reusa.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Editor de vídeo (trocar início/fim, re-render) — destino: nenhum, não é pedido.
|
||||
- Lixeira/desfazer e restauração de apagado (soft delete) — reversibilidade fica fora desta fatia.
|
||||
- Ações em lote (apagar/tentar vários).
|
||||
- Métricas, comentários, coleções/kits.
|
||||
- Mudar a página pública além de a ação de apagar refletir nela.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Já que apaga, deixa uma lixeira pra desfazer."** Se alguém "só completar": soft delete, retenção e restauração. **Corte neste item:** apagar é definitivo, com confirmação; sem lixeira.
|
||||
- **"Corte falhado vira fila de reprocessamento automático."** Se alguém "só completar": worker/cron e retry em lote. **Corte neste item:** resolver no ponto da falha (diálogo de origem) com o retry que já existe; na biblioteca, no máximo um "Tentar novamente" manual.
|
||||
- **"Aproveita e mostra estatística de falhas."** Se alguém "só completar": dashboard de vaidade. **Corte neste item:** sem métrica.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Descartar corte falhado automaticamente ou manter com "Tentar novamente"?** **Opções:** A) a falha deixa de virar item de biblioteca e é resolvida onde nasceu (diálogo da fala, retry existente) | B) o falhado aparece na biblioteca com "Tentar novamente" acionável. **Recomendação: A, com B como piso** — falha não é acervo; se por algum motivo ela aparecer na biblioteca, aparece com retry, nunca como item morto. _(assumido — validar no gate)_
|
||||
- **Apagar só o falhado/não publicado ou qualquer corte com aviso?** **Opções:** A) só falhado/não publicado | B) qualquer corte, com aviso explícito pelo link público. **Recomendação: B** — a assessoria precisa poder remover um corte errado/duplicado mesmo publicado; o aviso cobre o contrato do link. _(assumido)_
|
||||
- **Onde fica a ação?** **Opções:** A) no detalhe | B) no item da lista e no detalhe. **Recomendação: B** — a lista é onde a limpeza acontece; a confirmação evita o clique errado. _(assumido)_
|
||||
- **Publicado, apagar é definitivo ou só despublica?** **Opções:** A) apaga de vez com aviso | B) redireciona para "Despublicar". **Recomendação: A** — despublicar é reversível e já existe; quem quer apagar quer remover. _(assumido)_
|
||||
|
||||
## Referências
|
||||
|
||||
- GitHub Issue: — (após `pnpm agent:register`)
|
||||
- Design UI (gate): `docs/plans/biblioteca-cortes-apagar-e-falhas-ui-design.html`
|
||||
- `docs/plans/c168-biblioteca-cortes.md` (anti-goal reaberto) · `docs/plans/c169-corte-vod-confiabilidade.md` · `docs/plans/c167-cortar-trecho-publicar.md`
|
||||
- Arquivos-chave (pista, não contrato): `src/collections/SpeechCut.ts`, `src/lib/speechCut.ts`, `src/utilities/speech/speechCutListUrl.ts`, `src/utilities/speech/speechCutPageData.ts`, `src/components/campaign/speech/SpeechCutLibraryCard.tsx`, `src/components/campaign/speech/SpeechCutDialog.tsx`, `src/app/(campaign)/campanha/(app)/comunicacao/acervo/cortes/[id]/page.tsx`, `src/app/(campaign)/campanha/actions/speech.ts`
|
||||
- Testes a tocar: `tests/unit/speechCutDialog.unit.spec.tsx`, `tests/unit/speechCutListUrl.unit.spec.ts`, `tests/int/speechCut.int.spec.ts`, `tests/e2e/campaignSpeechCut.e2e.spec.ts`
|
||||
- `AGENTS.md` / `AGENTS-campaign.md` — vertical Comunicação, gate do acervo, naming/i18n
|
||||
|
||||
## Self-score (shaping)
|
||||
|
||||
5/5 — (1) fatia = um outcome verificável (apagar um corte e não acumular falhados); (2) appetite ~1 dia declarado e o escopo cabe (encaixe na biblioteca, sem lixeira/fila/editor); (3) persona, job e aceite em linguagem de usuário, com a reabertura de anti-goal e o contrato do link público explicitados; (4) direção no codebase é hipótese com precedente nomeado, sem contrato; (5) zero decisão dura de engenharia (rota exata da ação e access ficam para o plano de implementação — aqui só se declara quem pode apagar, como decisão de produto).
|
||||
575
docs/plans/nav-comunicacao-sub-itens-junto-ao-pai-ui-design.html
Normal file
575
docs/plans/nav-comunicacao-sub-itens-junto-ao-pai-ui-design.html
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-BR" data-theme="campaign">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>Design — Navegação de Comunicação agrupada</title>
|
||||
<style type="text/tailwindcss">
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-ring: var(--ring);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
}
|
||||
|
||||
[data-theme='campaign'] {
|
||||
--background: #ffffff;
|
||||
--foreground: #1c1917;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1c1917;
|
||||
--primary: #c51414;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f5f5f4;
|
||||
--secondary-foreground: #292524;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #6b7280;
|
||||
--accent: #f4f4f5;
|
||||
--accent-foreground: #1c1917;
|
||||
--border: rgb(28 25 23 / 12%);
|
||||
--ring: #c51414;
|
||||
--sidebar: #fafaf9;
|
||||
--sidebar-foreground: #1c1917;
|
||||
--sidebar-accent: #f5f5f4;
|
||||
--sidebar-accent-foreground: #1c1917;
|
||||
--sidebar-border: rgb(28 25 23 / 12%);
|
||||
--sidebar-ring: #c51414;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
.scene-shadow {
|
||||
box-shadow: 0 18px 48px rgb(28 25 23 / 10%);
|
||||
}
|
||||
.drawer-shadow {
|
||||
box-shadow: 0 -16px 48px rgb(28 25 23 / 20%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-stone-100 text-foreground antialiased">
|
||||
<main class="mx-auto max-w-[1400px] space-y-12 px-5 py-10 md:px-8 md:py-14">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-medium text-primary">Design de gate · campanha</p>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight">Comunicação se lê como um bloco</h1>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
Pai e filhos ficam próximos, unidos por uma única linha de hierarquia. Só o destino mais
|
||||
específico recebe o estado ativo — no rail e no drawer.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- CENA A — sidebar desktop -->
|
||||
<section aria-labelledby="desktop-title">
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-muted-foreground">Cena A · desktop ~1280</p>
|
||||
<h2 id="desktop-title" class="mt-1 text-base font-semibold">
|
||||
Rail com agrupamento local, sem redesenhar a navegação
|
||||
</h2>
|
||||
</div>
|
||||
<p class="hidden text-xs text-muted-foreground md:block">Ativo: Biblioteca de cortes</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="scene-shadow mx-auto h-[760px] max-w-[1280px] overflow-hidden rounded-xl border border-border bg-background"
|
||||
>
|
||||
<div class="flex h-full">
|
||||
<aside
|
||||
class="flex w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground"
|
||||
>
|
||||
<div class="flex h-16 items-center gap-3 border-b border-sidebar-border px-4">
|
||||
<div
|
||||
class="grid size-8 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground"
|
||||
>
|
||||
S
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold">Campanha</p>
|
||||
<p class="truncate text-xs text-muted-foreground">Mesa de trabalho</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="min-h-0 flex-1 overflow-hidden px-2 py-3"
|
||||
aria-label="Navegação principal"
|
||||
>
|
||||
<ul class="m-0 list-none space-y-0.5 p-0 text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="/campanha"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 9-8 9 8" />
|
||||
<path d="M5 10v10h14V10" /></svg
|
||||
><span>Início</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1" /></svg
|
||||
><span>Quadro</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 10c0 5-8 12-8 12S4 15 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="2" /></svg
|
||||
><span>Municípios</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 6l6-3 6 3 6-3v15l-6 3-6-3-6 3V6Z" />
|
||||
<path d="M9 3v15M15 6v15" /></svg
|
||||
><span>Territórios</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m11 17 2 2a3 3 0 0 0 4-4l-3-3" />
|
||||
<path d="m13 7-2-2a3 3 0 0 0-4 4l1 1" />
|
||||
<path d="m8 12 4-4 4 4-4 4-4-4Z" /></svg
|
||||
><span>Lideranças</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M2 21v-2a7 7 0 0 1 14 0v2" />
|
||||
<path d="M16 3.1a4 4 0 0 1 0 7.8M22 21v-2a7 7 0 0 0-4-6.3" /></svg
|
||||
><span>Pessoas</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="5" width="18" height="16" rx="2" />
|
||||
<path d="M16 3v4M8 3v4M3 11h18" /></svg
|
||||
><span>Agenda</span></a
|
||||
>
|
||||
</li>
|
||||
|
||||
<!-- Bloco de Comunicação: gap zero pai→linha; filhos compactos -->
|
||||
<li class="pt-1">
|
||||
<a
|
||||
href="/campanha/comunicacao"
|
||||
class="flex h-8 items-center gap-3 rounded-md px-2 font-medium text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3Z" />
|
||||
<path d="M11.6 16.9 13 22H7l-1.3-7" />
|
||||
</svg>
|
||||
<span>Comunicação</span>
|
||||
</a>
|
||||
<ul
|
||||
class="m-0 ml-[22px] list-none border-l border-sidebar-border px-0.5 pb-1 pl-2.5 pt-0.5"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo"
|
||||
class="flex h-8 items-center rounded-md px-2 text-sm text-muted-foreground hover:bg-sidebar-accent hover:text-foreground"
|
||||
>Acervo de falas</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes"
|
||||
aria-current="page"
|
||||
class="flex h-8 items-center rounded-md bg-card px-2 text-sm font-medium text-primary ring-1 ring-border"
|
||||
>Biblioteca de cortes</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="border-t border-sidebar-border p-3">
|
||||
<div class="flex items-center gap-3 rounded-md px-1 py-2">
|
||||
<div class="size-8 rounded-full bg-secondary ring-1 ring-border"></div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">Pessoa da equipe</p>
|
||||
<p class="truncate text-xs text-muted-foreground">Assessoria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0 flex-1 bg-background">
|
||||
<header class="flex h-16 items-center justify-between border-b border-border px-8">
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Comunicação</p>
|
||||
<p class="text-sm font-medium">Biblioteca de cortes</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="size-8 rounded-lg bg-secondary"></span
|
||||
><span class="size-8 rounded-full bg-secondary"></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mx-auto max-w-4xl p-8">
|
||||
<div class="h-10 w-3/5 rounded-md border border-border"></div>
|
||||
<div class="mt-8 space-y-3">
|
||||
<div class="h-32 rounded-xl border border-border"></div>
|
||||
<div class="h-32 rounded-xl border border-border"></div>
|
||||
<div class="h-32 rounded-xl border border-dashed border-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA B — drawer mobile -->
|
||||
<section aria-labelledby="mobile-title">
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-medium text-muted-foreground">Cena B · mobile ~390</p>
|
||||
<h2 id="mobile-title" class="mt-1 text-base font-semibold">
|
||||
Drawer “Mais” com a mesma gramática de agrupamento
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="scene-shadow relative mx-auto h-[780px] w-full max-w-[390px] overflow-hidden rounded-[28px] border-[6px] border-stone-800 bg-background"
|
||||
>
|
||||
<header
|
||||
class="flex h-14 items-center justify-between bg-primary px-4 text-primary-foreground"
|
||||
>
|
||||
<span class="text-sm font-semibold">Campanha</span>
|
||||
<span class="size-8 rounded-full bg-white/20"></span>
|
||||
</header>
|
||||
<div class="space-y-3 p-4">
|
||||
<div class="h-20 rounded-xl bg-secondary"></div>
|
||||
<div class="h-24 rounded-xl border border-border"></div>
|
||||
<div class="h-24 rounded-xl border border-border"></div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-foreground/30"></div>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title"
|
||||
class="drawer-shadow absolute inset-x-0 bottom-0 max-h-[690px] rounded-t-2xl border-t border-border bg-background"
|
||||
>
|
||||
<div class="mx-auto mt-2 h-1.5 w-10 rounded-full bg-stone-300"></div>
|
||||
<div class="px-4 pb-3 pt-4">
|
||||
<h3 id="drawer-title" class="text-base font-semibold">Navegação</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Destinos da campanha</p>
|
||||
</div>
|
||||
|
||||
<nav class="max-h-[510px] overflow-hidden px-4 pb-3" aria-label="Mais destinos">
|
||||
<div class="space-y-1">
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-11 items-center gap-3 rounded-md px-3 text-sm font-medium hover:bg-muted"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m11 17 2 2a3 3 0 0 0 4-4l-3-3" />
|
||||
<path d="m13 7-2-2a3 3 0 0 0-4 4l1 1" /></svg
|
||||
>Lideranças</a
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-11 items-center gap-3 rounded-md px-3 text-sm font-medium hover:bg-muted"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M2 21v-2a7 7 0 0 1 14 0v2" /></svg
|
||||
>Pessoas</a
|
||||
>
|
||||
|
||||
<!-- Mesmo bloco: pai toca visualmente a haste; os dois filhos ficam juntos. -->
|
||||
<div class="pt-1">
|
||||
<a
|
||||
href="/campanha/comunicacao"
|
||||
class="flex h-11 items-center gap-3 rounded-md px-3 text-sm font-medium text-foreground hover:bg-muted"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="size-5 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3Z" />
|
||||
<path d="M11.6 16.9 13 22H7l-1.3-7" />
|
||||
</svg>
|
||||
<span>Comunicação</span>
|
||||
</a>
|
||||
<div class="ml-[22px] border-l border-border pb-1 pl-3 pt-0.5">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo"
|
||||
class="flex min-h-10 items-center rounded-md px-3 text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>Acervo de falas</a
|
||||
>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes"
|
||||
aria-current="page"
|
||||
class="mt-1 flex min-h-10 items-center rounded-md bg-card px-3 text-sm font-medium text-primary ring-1 ring-border"
|
||||
>Biblioteca de cortes</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="flex h-11 items-center gap-3 rounded-md px-3 text-sm font-medium hover:bg-muted"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z" /></svg
|
||||
>Conceitos</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="border-t border-border px-4 py-3">
|
||||
<a href="#" class="flex h-11 items-center gap-3 rounded-md px-3 text-sm font-medium"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="8" r="5" />
|
||||
<path d="M20 21a8 8 0 0 0-16 0" /></svg
|
||||
>Perfil</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA C — comparação de estados -->
|
||||
<section aria-labelledby="states-title">
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-medium text-muted-foreground">Cena C · estados do sub-item</p>
|
||||
<h2 id="states-title" class="mt-1 text-base font-semibold">
|
||||
Uma forma compartilhada; contraste suficiente para um único ativo
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 lg:grid-cols-2">
|
||||
<article class="rounded-xl border border-border bg-sidebar p-5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Inativo</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Texto quieto, sem contorno ou preenchimento.
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="rounded-full border border-border bg-background px-2 py-1 text-xs text-muted-foreground"
|
||||
>estado padrão</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-5 max-w-sm">
|
||||
<a
|
||||
href="/campanha/comunicacao"
|
||||
class="flex h-9 items-center gap-3 rounded-md px-2 text-sm font-medium"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3Z" /></svg
|
||||
>Comunicação</a
|
||||
>
|
||||
<div class="ml-[22px] border-l border-sidebar-border pl-2.5 pt-0.5">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo"
|
||||
class="flex h-8 items-center rounded-md px-2 text-sm text-muted-foreground"
|
||||
>Acervo de falas</a
|
||||
>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes"
|
||||
class="flex h-8 items-center rounded-md px-2 text-sm text-muted-foreground"
|
||||
>Biblioteca de cortes</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="rounded-xl border border-border bg-sidebar p-5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Ativo</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Vermelho + peso médio + hairline, apenas no filho exato.
|
||||
</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-primary px-2 py-1 text-xs text-primary-foreground"
|
||||
>mais específico</span
|
||||
>
|
||||
</div>
|
||||
<div class="mt-5 max-w-sm">
|
||||
<a
|
||||
href="/campanha/comunicacao"
|
||||
class="flex h-9 items-center gap-3 rounded-md px-2 text-sm font-medium"
|
||||
><svg
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m3 11 18-5v12L3 14v-3Z" /></svg
|
||||
>Comunicação</a
|
||||
>
|
||||
<div class="ml-[22px] border-l border-sidebar-border pl-2.5 pt-0.5">
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo"
|
||||
class="flex h-8 items-center rounded-md px-2 text-sm text-muted-foreground"
|
||||
>Acervo de falas</a
|
||||
>
|
||||
<a
|
||||
href="/campanha/comunicacao/acervo/cortes"
|
||||
aria-current="page"
|
||||
class="flex h-8 items-center rounded-md bg-card px-2 text-sm font-medium text-primary ring-1 ring-border"
|
||||
>Biblioteca de cortes</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 rounded-lg border border-border bg-background p-4 text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
Destinos preservados:
|
||||
<code class="rounded bg-muted px-1.5 py-0.5 text-foreground">/campanha/comunicacao</code>,
|
||||
<code class="rounded bg-muted px-1.5 py-0.5 text-foreground"
|
||||
>/campanha/comunicacao/acervo</code
|
||||
>
|
||||
e
|
||||
<code class="rounded bg-muted px-1.5 py-0.5 text-foreground"
|
||||
>/campanha/comunicacao/acervo/cortes</code
|
||||
>.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
92
docs/plans/nav-comunicacao-sub-itens-junto-ao-pai.md
Normal file
92
docs/plans/nav-comunicacao-sub-itens-junto-ao-pai.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Navegação: os sub-itens de Comunicação ficam junto do pai
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1106
|
||||
Priority: P3
|
||||
Impeccable: B — encaixe na navegação da campanha (sidebar desktop + drawer overflow mobile)
|
||||
Design UI: docs/plans/nav-comunicacao-sub-itens-junto-ao-pai-ui-design.html
|
||||
Appetite: ~0,25–0,5 dia eng; um outcome verificável: numa olhada, "Acervo de falas" e "Biblioteca de cortes" se leem como filhos de "Comunicação", não como destinos distantes.
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
O C174 aninhou os dois destinos da vertical de comunicação sob "Comunicação", mas a associação visual ficou fraca: "Acervo de falas" e "Biblioteca de cortes" aparecem como entradas soltas em vez de filhos do pai, e a assessoria perde a hierarquia que o aninhamento queria criar. O agravante é que os dois consumidores — sidebar desktop e drawer overflow mobile — desenham a sub-lista por caminhos diferentes (um usa os primitivos `SidebarMenuSub`, o outro uma indentação própria), então a leitura pode divergir entre telas sem ninguém notar. O pedido é estreito: fazer o par se ler como filho do pai, de forma igual nas duas superfícies, sem mexer em destinos, permissões ou no desenho geral da navegação.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** assessoria de comunicação (`communicator`, coordenação/candidatura) navegando em `/campanha`, muitas vezes no celular, em dia de produção de peça.
|
||||
- **Job principal:** reconhecer de imediato que "Acervo de falas" e "Biblioteca de cortes" pertencem a "Comunicação".
|
||||
- **Fluxo desejado:** abre o menu → vê "Comunicação" com seus dois filhos logo abaixo, agrupados, como um bloco → escolhe o destino → o sub-item ativo é apenas o mais específico.
|
||||
- **Anti-goals de produto:** não é redesenho do rail nem novo nível de menu; não vira organograma de destinos.
|
||||
|
||||
### Esboço de fluxo (B)
|
||||
|
||||
```text
|
||||
[menu aberto] → "Comunicação" lê-se como pai com bloco de 2 filhos logo abaixo
|
||||
→ desktop (sidebar) e mobile (drawer overflow) mostram a MESMA relação
|
||||
→ toca "Biblioteca de cortes" → só esse sub-item fica ativo (acervo não acende junto)
|
||||
```
|
||||
|
||||
### Design UI (B)
|
||||
|
||||
- Design UI (gate): `docs/plans/nav-comunicacao-sub-itens-junto-ao-pai-ui-design.html` — cenas: sidebar desktop (pai + par de filhos, estados ativo/inativo) e drawer overflow mobile (~390px), com a leitura de agrupamento alinhada entre as duas.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- O par de sub-itens se lê como **filho do pai**: agrupamento visual claro, indentação e proximidade coerentes com "Comunicação".
|
||||
- A **mesma leitura** vale no desktop e no drawer mobile.
|
||||
- O estado ativo do sub-item continua destacando **só o mais específico** (`activeCampaignSubItemHref`).
|
||||
- **Nenhum destino some, muda de href ou ganha ícone.**
|
||||
- **Guardrails:** não renomear rotas/hrefs; não mudar permissões (`communicator`/staff); não introduzir segundo nível além do existente; não redesenhar a navegação inteira.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — é navegação; nenhum número ou agregado é exibido.
|
||||
- **Decisões desbloqueadas:** a assessoria decide rápido em qual destino da vertical entrar, sem procurar o que já está sob o pai.
|
||||
- **Forma:** _adiada ao plano de implementação_ — restrição: sem badges/contadores novos.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- Destinos e títulos exatos (inalterados): **"Acervo de falas"** → `CAMPAIGN_COMMUNICATION_ACERVO` (`/campanha/comunicacao/acervo`); **"Biblioteca de cortes"** → `CAMPAIGN_COMMUNICATION_CORTES` (`/campanha/comunicacao/acervo/cortes`).
|
||||
- Pai: **"Comunicação"** → `CAMPAIGN_COMMUNICATION_HOME` (`/campanha/comunicacao`).
|
||||
- O drawer mobile hoje usa indentação própria (`ml-5 border-l pl-3`); o desktop usa `SidebarMenuSub` (`mx-3.5 border-l px-2.5`). O item decide se os dois **convergem para uma leitura só**.
|
||||
- Fonte única dos títulos/hrefs: `communicationSubItems` em `src/components/campaign/shell/nav.ts` — não duplicar a lista.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `src/components/campaign/shell/nav.ts` (fonte única), `CampaignSidebar.tsx` (desktop), `CampaignBottomNav.tsx` (drawer), `src/components/ui/Sidebar.tsx` (classes base), `src/lib/campaignPaths.ts`.
|
||||
- **Precedente a olhar:** C174 em [`acervo-encontrar-cortes.md`](acervo-encontrar-cortes.md) — foi quem introduziu o aninhamento e o comentário "mesma forma nas duas superfícies".
|
||||
- **Risco de acoplamento:** não regredir o `activeCampaignSubItemHref` (irmãos com prefixo compartilhado); o "como" converge aqui.
|
||||
|
||||
## Dependências
|
||||
|
||||
- C174 (entregue — introduziu o aninhamento).
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Mudar a ordem dos destinos; novo item de navegação; badges/contadores; redesenho do rail; qualquer mudança em rotas, permissões ou ícones.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **"Já que vamos mexer, agrupa tudo por seções."** Se alguém "só completar": segundo nível novo, recolhimento por grupo, reordenação. **Corte neste item:** só a relação pai↔filhos existente fica legível.
|
||||
- **"Faz o rail inteiro colapsável com animação."** Se alguém "só completar": redesenho do shell. **Corte neste item:** encaixe local no desenho atual.
|
||||
- **"Alinha desktop e mobile reescrevendo o drawer com os primitivos."** Se alguém "só completar": troca estrutural do drawer. **Corte neste item:** a decisão é de leitura; o caminho técnico é da implementação.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Como aproximar?** **Opções:** A) só espaçamento/indentação no desenho atual | B) usar os primitivos shadcn também no drawer. **Recomendação:** garantir a hierarquia legível **nos dois**; o "como" fica na implementação. _(assumido — validar com produto)_
|
||||
- **O sub-item ativo ganha marcador extra?** **Opções:** A) manter o padrão atual | B) adicionar marcador. **Recomendação:** A — manter o padrão atual. _(assumido)_
|
||||
|
||||
## Referências
|
||||
|
||||
- GitHub Issue: — (a registrar)
|
||||
- Design UI (gate): `docs/plans/nav-comunicacao-sub-itens-junto-ao-pai-ui-design.html`
|
||||
- Planos irmãos: [`acervo-encontrar-cortes.md`](acervo-encontrar-cortes.md) (C174), [`remodelagem-municipios.md`](remodelagem-municipios.md) (nav), [`acervo-cortes-divida-pos-c174.md`](acervo-cortes-divida-pos-c174.md) (defer da sub-lista duplicada).
|
||||
- Arquivos-chave (pista, não contrato): `src/components/campaign/shell/nav.ts`, `src/components/campaign/shell/CampaignSidebar.tsx`, `src/components/campaign/shell/CampaignBottomNav.tsx`, `src/components/ui/Sidebar.tsx`, `src/lib/campaignPaths.ts`
|
||||
- Testes a olhar: `tests/unit/campaignNav.unit.spec.ts`, `tests/e2e/campaignBottomNav.e2e.spec.ts`
|
||||
- `AGENTS.md` / `AGENTS-campaign.md` — shell da campanha e convenções de navegação.
|
||||
|
||||
## Self-score (shaping)
|
||||
|
||||
4/5 — (1) fatia = um outcome verificável (o par se lê como filho do pai, igual nas duas superfícies); (2) appetite de ~0,25–0,5 dia comporta um encaixe de navegação; (3) persona, job e aceite em linguagem de produto; (4) direção no codebase é hipótese revisável; (5) zero decisão dura de engenharia — o "como" (espaçamento vs primitivos) fica explicitamente para a implementação.
|
||||
87
docs/plans/sollinha-fechado-por-padrao-no-desktop.md
Normal file
87
docs/plans/sollinha-fechado-por-padrao-no-desktop.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Sollinha começa fechado no desktop
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1107
|
||||
Priority: P2
|
||||
Impeccable: A — N/A sem nova superfície (muda o estado inicial de um painel que já existe; o estado fechado já é desenhado)
|
||||
Design UI: N/A — sem UI nova
|
||||
Appetite: ~0,25–0,5 dia eng; um outcome verificável: abrir `/campanha` no desktop em uma sessão nova mostra o conteúdo em largura cheia, com o Sollinha fechado; quem quiser abre pelo botão/FAB como hoje.
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
Hoje, numa sessão nova no desktop, o painel do Sollinha abre sozinho ocupando ~25% da largura. Quem abre `/campanha` para trabalhar no conteúdo perde largura sem ter pedido: o chat se impõe antes de qualquer intenção de usá-lo. O pedido é inverter o padrão — começar fechado e abrir sob comando. O FAB e o botão no header continuam abrindo normalmente.
|
||||
|
||||
## Persona e fluxo
|
||||
|
||||
- **Persona / contexto:** staff (coordinator, candidate, advisor, communicator) no desktop em `/campanha`, em trabalho de mesa — leitura de dados, edição, navegação entre municípios.
|
||||
- **Job principal:** ter a tela em largura cheia por padrão e chamar o Sollinha quando precisar.
|
||||
- **Fluxo desejado:** entra em `/campanha` numa sessão nova → vê o conteúdo em largura cheia, com o Sollinha fechado e o botão/FAB visível → se quiser, abre pelo botão/FAB → a conversa aparece e o painel se comporta como hoje → se fechar, a escolha fica lembrada na sessão; só uma sessão nova volta ao padrão fechado.
|
||||
- **Anti-goals de produto:** não virar redesenho do painel; não virar onboarding/hint obrigatório do chat; não alterar o comportamento mobile.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- Sessão nova no desktop inicia com o Sollinha fechado e o conteúdo em largura cheia.
|
||||
- Abrir pelo FAB/botão funciona como hoje e restaura a conversa.
|
||||
- A escolha do usuário (aberto/fechado) segue lembrada dentro da sessão — não forçar fechado em cima da escolha de quem já abriu.
|
||||
- Mobile intocado: drawer continua fechado por padrão e só abre sob comando.
|
||||
- **Guardrail:** nenhum estado "meio aberto" — fechado é fechado, aberto é aberto, sem largura residual.
|
||||
- **Guardrail:** o chat não perde conversa por começar fechado; o botão de abrir continua descobrível.
|
||||
- **Guardrail:** a convenção de largura do painel aberto (B166/B167) e a persistência de sessão (B199) permanecem; a mudança é só o default de sessão nova, não o estado restaurado.
|
||||
|
||||
## Dados (intenção)
|
||||
|
||||
- **Vou apresentar dados?** Não — nenhum número novo na tela.
|
||||
- **Decisões desbloqueadas:** N/A — o item muda estado inicial de um painel, não habilita leitura de dado.
|
||||
- **Forma:** _adiada ao plano de implementação_.
|
||||
|
||||
## Dados da decisão (literais)
|
||||
|
||||
- Largura atual do painel: `CHAT_DEFAULT_PCT='25'`.
|
||||
- Estado inicial de `open`: nasce `false`.
|
||||
- O efeito de settle (`measured && !isMobile && !restoredSessionRef.current && !panel.isCollapsed()`) é o que força o aberto hoje.
|
||||
- Sessão guarda `open` com `openBy: 'user'|'settle'`; ausência de sessão é o único sinal de sessão nova.
|
||||
- O estado fechado é o que já existe quando o usuário fecha — esta entrega reusa esse estado, não cria um novo.
|
||||
- A mudança é sobre o **default de sessão nova**, não sobre o estado restaurado.
|
||||
|
||||
## Direção no codebase (hipótese)
|
||||
|
||||
- **Áreas prováveis:** `CampaignAISidebarContext.tsx` (efeito de settle), `CampaignAISidebarShell.tsx` (`CHAT_DEFAULT_PCT` / `chatVisible` / painel), `sollinhaChatSession.ts` (default de sessão nova), e os testes e2e que pinam o comportamento desktop-open.
|
||||
- **Precedente a olhar:** `docs/plans/largura-padrao-chat-sollinha.md` (B166/B167), `docs/plans/sollinha-close-mid-stream-persist.md` (B199), `docs/plans/ai-chat-sollinha.md`.
|
||||
- **Risco de acoplamento:** não quebrar o restore de sessão nem o caminho mobile; o frame de hidratação e o desenho de largura já existentes precisam continuar coerentes.
|
||||
|
||||
## Dependências
|
||||
|
||||
- B167/B188/B199 (entregues — sessão, largura, persistência).
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Redesenhar o chat do Sollinha.
|
||||
- Mudar a largura padrão quando aberto.
|
||||
- Mexer no comportamento mobile.
|
||||
- Trocar a skill/agente Sollinha.
|
||||
|
||||
## Rabbit holes de produto
|
||||
|
||||
- **Vira "redesenho do painel".** Se alguém "só completar": repensar layout, animação, trigger do FAB, botão do header. **Corte neste item:** a entrega é só o default inicial; nada de novo desenho.
|
||||
- **Vira "hint de primeira vez".** Se alguém "só completar": tooltip, popover, checklist de descoberta do chat. **Corte neste item:** o FAB/header já é o convite (ver Questões em aberto).
|
||||
- **Vira "estado meio aberto".** Se alguém "só completar": largura residual, painel colapsado visível, resize persistido no fechado. **Corte neste item:** fechado é fechado, sem resíduo.
|
||||
|
||||
## Questões em aberto (produto)
|
||||
|
||||
- **Manter o painel com largura 25% mas escondido (CSS) vs. nascer colapsado/0?** **Opções:** A) manter largura e esconder; B) nascer colapsado em 0. **Recomendação:** A — menor risco, preserva o restauro e o desenho de largura já entregue (B166/B167). _(assumido — validar com produto)_
|
||||
- **Avisar o usuário que o chat existe?** **Opções:** A) não avisar; B) hint de primeira vez. **Recomendação:** A — o FAB e o botão do header já são o convite; um hint é escopo novo. _(assumido — validar com produto se querem hint de primeira vez.)_
|
||||
|
||||
## Referências
|
||||
|
||||
- Design UI (gate): N/A
|
||||
- `docs/plans/ai-chat-sollinha.md`
|
||||
- `docs/plans/sollinha-close-mid-stream-persist.md` (B199)
|
||||
- `docs/plans/largura-padrao-chat-sollinha.md` (B166/B167)
|
||||
- `AGENTS.md` / `AGENTS-campaign.md`
|
||||
- Testes que pinam o default atual: `tests/e2e/campaignAiChatResize.e2e.spec.ts:97-124`, `tests/e2e/campaignSollinhaWidth.e2e.spec.ts:40-56`, `tests/e2e/campaignSollinhaContext.e2e.spec.ts:140-199,283-327`, `tests/unit/sollinhaChatSession.unit.spec.ts:61,82,92,100`, `tests/unit/sollinhaChatPanelWidth.unit.spec.ts`
|
||||
|
||||
## Self-score (shaping)
|
||||
|
||||
5/5 — (1) fatia = um outcome verificável (sessão nova desktop abre com chat fechado e conteúdo em largura cheia); (2) appetite ~0,25–0,5 dia, mexe só no default inicial e no que pina o teste; (3) persona + job + aceite claros; (4) direção no codebase é hipótese, sem contrato/signature; (5) zero decisões duras de engenharia.
|
||||
Loading…
Reference in a new issue