docs(plan): C172–C178 + OPS122 — acervo/cortes, player e launch do worktree

Planos de intenção + designs hi-fi do gate para o lote pós-teste do dono:
- C178 #1081 player do YouTube sem parede de signin (defect, P1)
- C172 #1082 player/link no ponto certo da fala (defect, P1)
- C173 #1083 pré-visualizar o trecho selecionado (P2)
- C174 #1084 encontrar cortes (nav, fala, busca) (P2)
- C175 #1085 preview do trecho na listagem (P2)
- C176 #1086 página pública do corte — design (P2)
- C177 #1087 'Abrir fonte' só com fonte oficial (chore, P3)
- OPS122 #1088 launch do worktree honra modelo e --auto (defect, P2)

Related #1081, #1082, #1083, #1084, #1085, #1086, #1087, #1088
This commit is contained in:
Francisco Solla 2026-09-16 10:41:04 -03:00
parent b0582d5011
commit e1c9f33891
13 changed files with 2351 additions and 0 deletions

View file

@ -0,0 +1,90 @@
# Acervo: "Abrir fonte" só quando existe a fonte oficial (fim do atalho duplicado)
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1087
Priority: P3
Impeccable: A — N/A sem nova superfície (remoção/ajuste de um botão)
Design UI: N/A — sem UI
Appetite: ~0,25–0,5 dia eng; um outcome verificável — nenhum botão do acervo leva a um vídeo sem marcar o ponto
Responsável: —
## Intenção
O acervo de falas mostra o botão "Abrir fonte" desde antes do C160. Depois que "Abrir no YouTube" chegou (com o timestamp da fala), o usuário relatou: *"O botão 'abrir fonte' se tornou inutil, já que agora temos o 'Abrir no Youtube', podemos remove-lo. Inclusive o link atual do 'Abrir fonte' vai para o video sem marcar o inicio, o que pode confundir o usuario."*
O defeito concreto: quando a fala tem o PDF oficial do Diário (`officialTextUrl`), o botão está certo e aponta para a fonte oficial. Quando o import **não** resolveu o Diário, o código cai num fallback (`view.officialTextUrl ?? view.youtubeUrl`) e o botão abre o **vídeo cru no YouTube, sem timestamp** — exatamente o que "Abrir no YouTube" já faz melhor. Ou seja, hoje o mesmo botão às vezes é proveniência oficial e às vezes é um atalho pior e duplicado. Este item faz "Abrir fonte" significar uma coisa só: a fonte oficial. Sem fonte oficial, o botão não aparece.
## Persona e fluxo
- **Persona / contexto:** assessoria/comunicação e staff conferindo proveniência de uma fala no acervo; na mesa, com o detalhe ou a lista abertos, querem checar/citar a publicação oficial.
- **Job principal:** abrir a fonte oficial de uma fala quando ela existe, sem que um botão prometa "fonte" e entregue um vídeo sem o ponto.
- **Fluxo desejado:** abre a fala → se há Diário oficial, vê "Abrir Diário Oficial" e cai no PDF; se não há, o botão simplesmente não existe e "Abrir no YouTube" (no ponto da fala) é o caminho de vídeo.
- **Anti-goals de produto:** transformar "Abrir fonte" num segundo link de vídeo; esconder que a fonte oficial faltou com um link pior; mexer no "Abrir no YouTube" timestampado; redesenhar a tela; reimportar dados.
## Objetivo e aceite
- Nenhum botão rotulado como fonte aponta para uma URL de vídeo sem marcação de ponto.
- "Abrir fonte" (ou o rótulo substituto) só é renderizado quando `officialTextUrl` existe; sem ele, some — não cai no `youtubeUrl`.
- "Abrir no YouTube" permanece intocado, abrindo no ponto da fala.
- Acesso ao PDF oficial não é perdido silenciosamente onde ele existe (C160 preservado).
- Lista e detalhe se comportam igual: o mesmo critério de existência nos dois.
- **Guardrails:** sem schema/Consent/migration; sem tocar no VOD/MP4 nem no crédito "Fonte: Câmara dos Deputados · CC BY 4.0"; URL pública do acervo intocada; sem mudança de import.
## Dados (intenção)
- **Vou apresentar dados?** Não — a superfície é um botão existente; nenhum número, série ou agregado é apresentado ou derivado.
- **Decisões desbloqueadas:** _N/A — sem métrica; a decisão do usuário (checar a fonte oficial) já existe e é servida ou não pelo botão._
- **Forma:** _adiada ao plano de implementação_ — restrição de produto: o critério é "existe fonte oficial?", nunca "existe algum link?".
## Dados da decisão (literais)
- **Condição de exibição:** renderizar o botão **somente** quando `officialTextUrl` for não-nulo; remover o fallback `?? youtubeUrl` nos dois pontos: `src/app/(campaign)/campanha/(app)/comunicacao/acervo/[id]/page.tsx` e `src/utilities/speech/speechViewModels.ts`.
- **Rótulo proposto (pt-BR):** `Abrir Diário Oficial` (alternativas no gate: `Ver Diário Oficial`, `Abrir PDF oficial`) — deixa claro que é o registro oficial e não um vídeo.
- **Não mexer:** o botão/atalho "Abrir no YouTube" (`SpeechDetailPlayer.tsx`) e "Assistir na Câmara" seguem como estão, com timestamp.
- **Fallback proibido:** `youtubeUrl` nunca é destino do botão de fonte; `sourceUrl` deixa de representar "fonte ou vídeo".
- **Onde mais aparece:** card da lista `SpeechResultCard.tsx` — mesmo critério.
- **Comportamento do C160 preservado:** quando `officialTextUrl` existe, o destino continua sendo o PDF direto do Diário.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/campaign/speech/SpeechDetailPlayer.tsx` (render do botão, props), `src/components/campaign/speech/SpeechResultCard.tsx` (card da lista), `src/utilities/speech/speechViewModels.ts` (derivação de `sourceUrl`), `src/app/(campaign)/campanha/(app)/comunicacao/acervo/[id]/page.tsx` (passa a prop).
- **Precedente a olhar:** C160 `docs/plans/acervo-abrir-fonte-pdf-oficial.md` (dono do destino do link); `docs/plans/acervo-videos-comunicacao.md` (C154, atalho YouTube timestampado).
- **Risco de acoplamento:** `sourceUrl` pode ser lido por testes/e2e (`tests/e2e/campaignSpeechAcervo.e2e.spec.ts`) — a mudança de nome/semântica precisa acompanhar; não deixar a lista e o detalhe divergirem.
## Dependências
- Entregues (duras, satisfeitas): C160/#988 (PDF oficial como destino de "Abrir fonte"); C154 (acervo + "Abrir no YouTube" timestampado).
- Nenhuma dependência bloqueante.
## Fora de escopo
- Remoção total do botão (decisão de gate — ver Questões em aberto; ficaria como follow-up se escolhida).
- Backfill/resolução de Diários ausentes (é do C160; aqui não se cria fonte onde não há).
- Qualquer mudança no VOD/MP4, trechos, transcrições, import ou crédito CC BY.
- Redesenho da tela do acervo ou introdução de novo campo de fonte.
## Rabbit holes de produto
- **"Preencher o `officialTextUrl` que falta para todo mundo ter botão."** Se alguém "só completar": puxa a resolução offline do C160 de novo e um backfill — escopo de outro item. **Corte neste item:** sem fonte oficial, o botão não aparece.
- **"Manter o fallback mas com timestamp."** Se alguém "só completar": o botão de fonte vira clone do "Abrir no YouTube" — a duplicação que o usuário reclamou. **Corte neste item:** fonte é fonte; vídeo é vídeo.
- **"Aproveitar e remover o botão de vez."** Se alguém "só completar": some o acesso de um clique ao PDF oficial e o C160 perde sentido. **Corte neste item:** decisão consciente no gate, não efeito colateral.
## Questões em aberto (produto)
- **Remover o botão de vez (pedido literal) ou só endurecer a condição?** **Opções:** A) remoção total — perde o atalho ao PDF oficial onde ele existe; B) manter só quando há `officialTextUrl`, com rótulo explícito, sem fallback YouTube; C) manter os dois (fonte + YouTube) como hoje. **Recomendação:** B — resolve a confusão relatada (fim do vídeo sem ponto) sem jogar fora o acesso ao Diário oficial. _(decidido — produto 2026-09-16: manter apenas quando existe a fonte oficial; sem fallback de vídeo)_
- **Qual rótulo?** **Opções:** A) `Abrir Diário Oficial` | B) `Ver Diário Oficial` | C) `Abrir PDF oficial`. **Recomendação:** A — nomeia o registro oficial e mantém o verbo do botão atual. _(assumido)_
- **Lista e detalhe com o mesmo texto?** **Opções:** A) sim, mesmo rótulo | B) lista mais curto. **Recomendação:** A — uma regra só, menos surpresa. _(assumido)_
## Referências
- GitHub Issue #1087
- Design UI (gate): N/A — sem UI
- `src/components/campaign/speech/SpeechDetailPlayer.tsx` (botão "Abrir fonte"; "Abrir no YouTube")
- `src/components/campaign/speech/SpeechResultCard.tsx` · `src/utilities/speech/speechViewModels.ts` · `src/app/(campaign)/campanha/(app)/comunicacao/acervo/[id]/page.tsx`
- `docs/plans/acervo-abrir-fonte-pdf-oficial.md` (C160/#988) · `docs/plans/acervo-videos-comunicacao.md` (C154)
- `AGENTS.md` — convenções de i18n (rótulo pt-BR) e rotas `/campanha`
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável (nenhum botão de fonte leva a vídeo sem ponto); (2) appetite ~0,25–0,5 dia, dois sítios de render; (3) persona + job + aceite claros; (4) direção no codebase é hipótese; (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,251 @@
<!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 — C174 · Acervo: encontrar cortes</title>
<style type="text/tailwindcss">
@theme inline {
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--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-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-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-support-engaged: var(--support-engaged);
--color-support-engaged-foreground: var(--support-engaged-foreground);
--color-estimate-pending: var(--estimate-pending);
--color-estimate-pending-foreground: var(--estimate-pending-foreground);
}
[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;
--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-accent-foreground: #1c1917;
--sidebar-border: rgb(28 25 23 / 12%);
--support-engaged: #dcfce7;
--support-engaged-foreground: #166534;
--estimate-pending: #fef3c7;
--estimate-pending-foreground: #92400e;
}
* { box-sizing: border-box; }
body { font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
button, a { -webkit-tap-highlight-color: transparent; }
.artifact-grid {
background-image: radial-gradient(rgb(28 25 23 / 9%) 0.7px, transparent 0.7px);
background-size: 16px 16px;
}
.frame-shadow { box-shadow: 0 18px 54px rgb(28 25 23 / 10%); }
.cut-row-dashed { border-style: dashed; }
</style>
</head>
<body class="artifact-grid min-h-screen bg-[#efeeec] text-foreground antialiased">
<main class="mx-auto flex max-w-[1480px] flex-col gap-16 px-4 py-10 md:px-8 md:py-14">
<header class="max-w-3xl">
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-full bg-primary px-2.5 py-1 text-xs font-semibold text-primary-foreground">C174 · gate hi-fi</span>
<span class="rounded-full border border-border bg-white px-2.5 py-1 text-xs font-medium">Campanha · Comunicação</span>
</div>
<h1 class="mt-4 text-3xl font-semibold tracking-tight">Encontrar cortes sem recortar de novo</h1>
<p class="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">A assessoria reencontra um corte pela navegação, pela fala de origem ou pela busca. Cenas estáticas para aprovar hierarquia, estados, densidade e responsividade.</p>
</header>
<!-- Cena 1: nav desktop -->
<section aria-labelledby="scene-nav">
<div class="mb-4 flex items-end justify-between gap-4">
<div>
<p class="text-xs font-semibold text-primary">CENA 01</p>
<h2 id="scene-nav" class="mt-1 text-xl font-semibold tracking-tight">Nav subitems · desktop · 256 px</h2>
</div>
<p class="hidden text-xs text-muted-foreground md:block">Duas variantes pequenas de estado ativo</p>
</div>
<div class="grid gap-5 lg:grid-cols-2">
<article class="overflow-hidden rounded-xl border border-border bg-white frame-shadow">
<div class="border-b border-border bg-muted/60 px-4 py-2.5 text-xs font-medium text-muted-foreground">Ativo: Acervo de falas</div>
<div class="flex min-h-[410px]">
<aside class="flex w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar p-3">
<div class="mb-5 flex h-10 items-center gap-2 px-2">
<div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div>
<div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div>
</div>
<nav class="space-y-1 text-sm">
<a class="flex h-9 items-center gap-3 rounded-md px-2 text-sidebar-foreground" href="#"><svg 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 class="flex h-9 items-center gap-3 rounded-md px-2 text-sidebar-foreground" href="#"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="m7 16 4-4 4 2 4-6"/></svg>Quadro</a>
<div class="rounded-lg bg-sidebar-accent p-1">
<div class="flex h-9 items-center gap-3 rounded-md px-2 font-medium text-sidebar-accent-foreground">
<svg class="size-4 text-primary" 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.8 13 21H8l-1.8-6"/></svg>
<span>Comunicação</span><svg class="ml-auto size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</div>
<div class="ml-5 border-l border-sidebar-border pl-3">
<a href="#" aria-current="page" class="flex h-9 items-center rounded-md bg-white px-2 text-sm font-medium text-primary ring-1 ring-border">Acervo de falas</a>
<a href="#" class="mt-1 flex h-9 items-center rounded-md px-2 text-sm text-muted-foreground hover:bg-white">Biblioteca de cortes</a>
</div>
</div>
</nav>
<div class="mt-auto border-t border-sidebar-border pt-3"><div class="flex items-center gap-3 px-2 py-2"><div class="size-8 rounded-full bg-secondary"></div><div><p class="text-sm font-medium">Assessoria</p><p class="text-xs text-muted-foreground">Comunicação</p></div></div></div>
</aside>
<div class="flex-1 bg-white p-6"><div class="h-4 w-36 rounded bg-muted"></div><div class="mt-5 h-10 rounded-md border border-input"></div><div class="mt-8 h-24 rounded-xl border border-border"></div></div>
</div>
</article>
<article class="overflow-hidden rounded-xl border border-border bg-white frame-shadow">
<div class="border-b border-border bg-muted/60 px-4 py-2.5 text-xs font-medium text-muted-foreground">Ativo: Biblioteca de cortes</div>
<div class="flex min-h-[410px]">
<aside class="flex w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar p-3">
<div class="mb-5 flex h-10 items-center gap-2 px-2">
<div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div>
<div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div>
</div>
<nav class="space-y-1 text-sm">
<a class="flex h-9 items-center gap-3 rounded-md px-2" href="#"><svg 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 class="flex h-9 items-center gap-3 rounded-md px-2" href="#"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="m7 16 4-4 4 2 4-6"/></svg>Quadro</a>
<div class="rounded-lg bg-sidebar-accent p-1">
<div class="flex h-9 items-center gap-3 rounded-md px-2 font-medium">
<svg class="size-4 text-primary" 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.8 13 21H8l-1.8-6"/></svg>
<span>Comunicação</span><svg class="ml-auto size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</div>
<div class="ml-5 border-l border-sidebar-border pl-3">
<a href="#" class="flex h-9 items-center rounded-md px-2 text-sm text-muted-foreground">Acervo de falas</a>
<a href="#" aria-current="page" class="mt-1 flex h-9 items-center rounded-md bg-white px-2 text-sm font-medium text-primary ring-1 ring-border">Biblioteca de cortes</a>
</div>
</div>
</nav>
<div class="mt-auto border-t border-sidebar-border pt-3"><div class="flex items-center gap-3 px-2 py-2"><div class="size-8 rounded-full bg-secondary"></div><div><p class="text-sm font-medium">Assessoria</p><p class="text-xs text-muted-foreground">Comunicação</p></div></div></div>
</aside>
<div class="flex-1 bg-white p-6"><div class="h-4 w-44 rounded bg-muted"></div><div class="mt-5 h-10 rounded-md border border-input"></div><div class="mt-8 h-24 rounded-xl border border-dashed border-border"></div></div>
</div>
</article>
</div>
</section>
<!-- Cena 2: detalhe populado -->
<section aria-labelledby="scene-detail-populated">
<div class="mb-4">
<p class="text-xs font-semibold text-primary">CENA 02</p>
<h2 id="scene-detail-populated" class="mt-1 text-xl font-semibold tracking-tight">Fala · desktop ~1280 · Cortes desta fala (populado)</h2>
</div>
<div class="overflow-x-auto rounded-xl border border-border bg-white frame-shadow">
<div class="flex min-h-[720px] min-w-[1180px]">
<aside class="w-64 shrink-0 border-r border-sidebar-border bg-sidebar p-3">
<div class="mb-5 flex h-10 items-center gap-2 px-2"><div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div><div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div></div>
<nav class="space-y-1 text-sm"><a class="flex h-9 items-center gap-3 rounded-md px-2" href="#">⌂ <span>Início</span></a><div class="rounded-lg bg-sidebar-accent p-1"><div class="flex h-9 items-center gap-3 px-2 font-medium"><span class="text-primary">⌁</span> Comunicação <span class="ml-auto">⌄</span></div><div class="ml-5 border-l border-sidebar-border pl-3"><a class="flex h-9 items-center rounded-md bg-white px-2 font-medium text-primary ring-1 ring-border" href="#">Acervo de falas</a><a class="mt-1 flex h-9 items-center rounded-md px-2 text-muted-foreground" href="#">Biblioteca de cortes</a></div></div></nav>
</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-base font-semibold">Pequeno Expediente</p><p class="text-xs text-muted-foreground">Data da fala · Câmara dos Deputados</p></div><div class="size-9 rounded-full bg-secondary"></div></header>
<div class="mx-auto flex max-w-[980px] flex-col gap-8 px-8 py-8">
<a href="#" class="-ml-2 flex h-10 w-fit items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-muted">← Voltar ao acervo</a>
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_240px]">
<div>
<h1 class="text-lg font-medium">Pequeno Expediente</h1>
<p class="mt-1 text-xs text-muted-foreground">Data da fala · Fase da sessão · 12:34</p>
<div class="mt-4 aspect-video overflow-hidden rounded-xl bg-[#292524] ring-1 ring-foreground/10">
<div class="flex h-full flex-col items-center justify-center gap-3 text-white"><div class="flex size-12 items-center justify-center rounded-full bg-white/15">▶</div><span class="rounded bg-black/45 px-2 py-1 text-xs font-medium">NEEDS ASSET — miniatura real do vídeo</span></div>
</div>
</div>
<aside class="space-y-5"><div><p class="text-xs text-muted-foreground">Sumário oficial</p><div class="mt-2 space-y-2"><div class="h-2.5 rounded bg-muted"></div><div class="h-2.5 w-4/5 rounded bg-muted"></div><div class="h-2.5 w-3/5 rounded bg-muted"></div></div></div><div><p class="text-xs text-muted-foreground">Temas</p><div class="mt-2 flex gap-1.5"><span class="rounded-full bg-secondary px-2 py-1 text-xs">Tema da fala</span><span class="rounded-full bg-secondary px-2 py-1 text-xs">Bahia</span></div></div></aside>
</div>
<section aria-labelledby="cuts-populated" class="border-t border-border pt-6">
<div class="flex items-end justify-between gap-4"><div><h2 id="cuts-populated" class="text-lg font-semibold tracking-tight">Cortes desta fala</h2><p class="mt-1 text-sm text-muted-foreground">Abra um corte existente para editar ou reaproveitar.</p></div></div>
<div class="mt-4 overflow-hidden rounded-xl border border-border">
<div class="grid grid-cols-[minmax(0,1fr)_130px_76px_112px] gap-3 border-b border-border bg-muted/60 px-4 py-2.5 text-xs font-medium text-muted-foreground"><span>Título</span><span>Status</span><span>Duração</span><span class="sr-only">Ação</span></div>
<div class="grid grid-cols-[minmax(0,1fr)_130px_76px_112px] items-center gap-3 border-b border-border px-4 py-3"><p class="truncate text-sm font-medium">Trecho selecionado da fala</p><span class="w-fit rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">Publicado</span><span class="text-xs text-muted-foreground">00:48</span><a href="#" class="flex h-9 items-center justify-center rounded-lg border border-border bg-white px-2.5 text-sm font-medium">Abrir corte</a></div>
<div class="grid grid-cols-[minmax(0,1fr)_130px_76px_112px] items-center gap-3 border-b border-border px-4 py-3"><p class="truncate text-sm font-medium">Declaração para redes</p><span class="w-fit rounded-full border border-border px-2 py-0.5 text-xs font-medium">Despublicado</span><span class="text-xs text-muted-foreground">01:06</span><a href="#" class="flex h-9 items-center justify-center rounded-lg border border-border bg-white px-2.5 text-sm font-medium">Abrir corte</a></div>
<div class="grid grid-cols-[minmax(0,1fr)_130px_76px_112px] items-center gap-3 border-b border-border px-4 py-3"><p class="truncate text-sm font-medium">Trecho em preparação</p><span class="w-fit rounded-full border border-border px-2 py-0.5 text-xs font-medium">Processando</span><span class="text-xs text-muted-foreground">00:36</span><a href="#" class="flex h-9 items-center justify-center rounded-lg border border-border bg-white px-2.5 text-sm font-medium">Abrir corte</a></div>
<div class="grid grid-cols-[minmax(0,1fr)_130px_76px_112px] items-center gap-3 px-4 py-3"><p class="truncate text-sm font-medium">Trecho com falha</p><span class="w-fit rounded-full bg-destructive/10 px-2 py-0.5 text-xs font-medium text-destructive">Falhou</span><span class="text-xs text-muted-foreground">00:29</span><a href="#" class="flex h-9 items-center justify-center rounded-lg border border-border bg-white px-2.5 text-sm font-medium">Abrir corte</a></div>
</div>
</section>
</div>
</div>
</div>
</div>
</section>
<!-- Cena 3: detalhe vazio -->
<section aria-labelledby="scene-detail-empty">
<div class="mb-4">
<p class="text-xs font-semibold text-primary">CENA 03</p>
<h2 id="scene-detail-empty" class="mt-1 text-xl font-semibold tracking-tight">Fala · desktop ~1280 · Cortes desta fala (vazio)</h2>
</div>
<div class="overflow-x-auto rounded-xl border border-border bg-white frame-shadow">
<div class="flex min-h-[610px] min-w-[1180px]">
<aside class="w-64 shrink-0 border-r border-sidebar-border bg-sidebar p-3"><div class="mb-5 flex h-10 items-center gap-2 px-2"><div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div><div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div></div><div class="rounded-lg bg-sidebar-accent p-1 text-sm"><div class="flex h-9 items-center gap-3 px-2 font-medium"><span class="text-primary">⌁</span> Comunicação <span class="ml-auto">⌄</span></div><div class="ml-5 border-l border-sidebar-border pl-3"><div class="flex h-9 items-center rounded-md bg-white px-2 font-medium text-primary ring-1 ring-border">Acervo de falas</div><div class="mt-1 flex h-9 items-center rounded-md px-2 text-muted-foreground">Biblioteca de cortes</div></div></div></aside>
<div class="flex-1"><header class="flex h-16 items-center border-b border-border px-8"><div><p class="text-base font-semibold">Grande Expediente</p><p class="text-xs text-muted-foreground">Data da fala · Câmara dos Deputados</p></div></header><div class="mx-auto max-w-[900px] px-8 py-8"><a href="#" class="-ml-2 flex h-10 w-fit items-center rounded-lg px-2 text-sm font-medium">← Voltar ao acervo</a><div class="mt-4 grid grid-cols-[minmax(0,1fr)_220px] gap-6"><div><h1 class="text-lg font-medium">Grande Expediente</h1><p class="mt-1 text-xs text-muted-foreground">Data da fala · 10:42</p><div class="mt-4 flex aspect-video items-center justify-center rounded-xl bg-[#292524] text-white"><span class="rounded bg-black/45 px-2 py-1 text-xs font-medium">NEEDS ASSET — miniatura real do vídeo</span></div></div><div class="space-y-2 pt-2"><div class="h-2.5 rounded bg-muted"></div><div class="h-2.5 rounded bg-muted"></div><div class="h-2.5 w-3/4 rounded bg-muted"></div></div></div>
<section class="mt-8 border-t border-border pt-6"><h2 class="text-lg font-semibold tracking-tight">Cortes desta fala</h2><div class="mt-4 flex min-h-52 flex-col items-center justify-center rounded-xl border border-border px-6 py-8 text-center"><div class="flex size-10 items-center justify-center rounded-full bg-muted"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="7" r="3"/><circle cx="6" cy="17" r="3"/><path d="m8.7 8.3 12.6 6.4"/><path d="m8.7 15.7 12.6-6.4"/></svg></div><h3 class="mt-3 text-base font-medium">Nenhum corte desta fala ainda</h3><p class="mt-1 text-sm text-muted-foreground">Selecione um trecho no player para criar o primeiro corte.</p><a href="#" class="mt-4 flex h-11 items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground">Selecionar trecho</a></div></section>
</div></div>
</div>
</div>
</section>
<!-- Cena 4: busca fala + corte -->
<section aria-labelledby="scene-search-speech">
<div class="mb-4"><p class="text-xs font-semibold text-primary">CENA 04</p><h2 id="scene-search-speech" class="mt-1 text-xl font-semibold tracking-tight">Busca no acervo · fala encontrada com corte aninhado</h2></div>
<div class="overflow-x-auto rounded-xl border border-border bg-white frame-shadow"><div class="flex min-h-[600px] min-w-[1180px]"><aside class="w-64 shrink-0 border-r border-sidebar-border bg-sidebar p-3"><div class="mb-5 flex h-10 items-center gap-2 px-2"><div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div><div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div></div><div class="rounded-lg bg-sidebar-accent p-1 text-sm"><div class="flex h-9 items-center px-2 font-medium">Comunicação <span class="ml-auto">⌄</span></div><div class="ml-5 border-l border-sidebar-border pl-3"><div class="flex h-9 items-center rounded-md bg-white px-2 font-medium text-primary ring-1 ring-border">Acervo de falas</div><div class="mt-1 flex h-9 items-center rounded-md px-2 text-muted-foreground">Biblioteca de cortes</div></div></div></aside>
<div class="flex-1"><header class="flex h-16 items-center border-b border-border px-8"><div><p class="text-base font-semibold">Acervo de falas</p><p class="text-xs text-muted-foreground">Encontre falas por tema, local ou palavra-chave</p></div></header><div class="mx-auto max-w-[900px] px-8 py-8"><form class="rounded-xl border border-border bg-white p-3"><label class="text-xs font-medium" for="desktop-search-a">Buscar fala por termo</label><div class="mt-2 flex h-11 items-center gap-2 rounded-md border border-input px-3"><svg class="size-4 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-a" class="w-full bg-transparent text-sm outline-none" value="tema pesquisado" readonly /></div><div class="mt-2 flex gap-2"><span class="rounded-full border border-border px-2.5 py-1 text-xs">Ano</span><span class="rounded-full border border-border px-2.5 py-1 text-xs">Tema</span><span class="rounded-full border border-border px-2.5 py-1 text-xs">Município</span></div></form>
<article class="mt-6 rounded-xl border border-border bg-card p-4"><div class="flex flex-wrap gap-x-2 text-xs text-muted-foreground"><span>Data da fala</span><span>·</span><span>Pequeno Expediente</span><span>·</span><span>12:34</span></div><p class="mt-2 text-sm leading-relaxed">… trecho da fala em que o <mark class="rounded-sm bg-estimate-pending px-0.5 text-foreground">tema pesquisado</mark> aparece no contexto …</p><div class="mt-3 flex gap-1.5"><span class="rounded-full bg-secondary px-2 py-0.5 text-xs">Tema da fala</span><span class="rounded-full border border-border px-2 py-0.5 text-xs text-muted-foreground">palavra-chave</span></div><a href="#" class="mt-3 flex h-11 w-fit items-center gap-2 rounded-lg bg-primary px-3 text-sm font-medium text-white">▶ Ver fala</a>
<div class="ml-5 mt-4 border-l border-border pl-4"><p class="mb-2 text-xs font-medium text-muted-foreground">1 corte desta fala</p><div class="rounded-lg bg-muted/55 p-3 ring-1 ring-border"><div class="flex flex-wrap items-center gap-2"><span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">Publicado</span><span class="text-xs text-muted-foreground">00:48</span></div><div class="mt-2 flex items-center justify-between gap-4"><p class="text-sm font-medium">Trecho sobre o tema pesquisado</p><a href="#" class="shrink-0 text-sm font-medium text-primary underline-offset-4 hover:underline">Abrir corte →</a></div></div></div>
</article><p class="mt-4 text-xs text-muted-foreground">1 fala encontrada</p></div></div>
</div></div>
</section>
<!-- Cena 5: opção B -->
<section aria-labelledby="scene-search-cut">
<div class="mb-4"><p class="text-xs font-semibold text-primary">CENA 05</p><h2 id="scene-search-cut" class="mt-1 text-xl font-semibold tracking-tight">Busca no acervo · corte casa, fala aparece como origem (opção B)</h2></div>
<div class="overflow-x-auto rounded-xl border border-border bg-white frame-shadow"><div class="min-w-[1180px] p-8"><div class="mx-auto max-w-[900px]"><div class="flex items-end justify-between gap-4"><div><h3 class="text-base font-semibold">Acervo de falas</h3><p class="text-xs text-muted-foreground">Resultado mantido em uma única lista</p></div><a href="#" class="flex h-11 items-center rounded-lg border border-border px-3 text-sm font-medium">Biblioteca de cortes</a></div><form class="mt-5"><label class="text-xs font-medium" for="desktop-search-b">Buscar fala por termo</label><div class="mt-2 flex h-11 items-center gap-2 rounded-md border border-primary px-3 ring-3 ring-primary/20"><span class="text-muted-foreground">⌕</span><input id="desktop-search-b" class="w-full bg-transparent text-sm outline-none" value="expressão do corte" readonly /></div></form>
<article class="mt-6 rounded-xl border border-border bg-card p-4"><div class="flex items-start justify-between gap-4"><div><div class="flex flex-wrap gap-x-2 text-xs text-muted-foreground"><span>Fala de origem</span><span>·</span><span>Data da fala</span><span>·</span><span>Grande Expediente</span></div><p class="mt-2 text-sm text-muted-foreground">A fala não contém o termo exato; ela aparece porque um corte vinculado corresponde à busca.</p></div><a href="#" class="flex h-11 shrink-0 items-center rounded-lg bg-primary px-3 text-sm font-medium text-white">Ver fala</a></div><div class="ml-5 mt-4 border-l border-border pl-4"><p class="mb-2 text-xs font-medium text-muted-foreground">Correspondência no corte</p><div class="rounded-lg bg-muted/55 p-3 ring-1 ring-border"><div class="flex flex-wrap items-center gap-2"><span class="rounded-full border border-border bg-white px-2 py-0.5 text-xs font-medium">Despublicado</span><span class="text-xs text-muted-foreground">01:06</span></div><h4 class="mt-2 text-sm font-medium">Título com a <mark class="rounded-sm bg-estimate-pending px-0.5 text-foreground">expressão do corte</mark></h4><p class="mt-1 text-xs text-muted-foreground">Descrição curta do corte correspondente.</p><a href="#" class="mt-3 inline-flex text-sm font-medium text-primary underline-offset-4 hover:underline">Abrir corte →</a></div></div></article><p class="mt-4 text-xs text-muted-foreground">1 fala encontrada por corte vinculado</p></div></div></div>
</section>
<!-- Cena 6: mobile -->
<section aria-labelledby="scene-mobile">
<div class="mb-4"><p class="text-xs font-semibold text-primary">CENA 06</p><h2 id="scene-mobile" class="mt-1 text-xl font-semibold tracking-tight">Mobile ~390 · resultado aninhado + seção da fala</h2></div>
<div class="flex flex-wrap items-start gap-8">
<article class="w-[390px] max-w-full overflow-hidden rounded-[18px] border border-border bg-white frame-shadow"><div class="flex h-14 items-center justify-between bg-primary px-4 text-white"><div class="flex items-center gap-2"><div class="flex size-8 items-center justify-center rounded-lg bg-white/15 text-sm font-bold">S</div><div><p class="text-sm font-semibold">Acervo de falas</p><p class="text-[11px] text-white/75">Comunicação</p></div></div><div class="size-8 rounded-full bg-white/15"></div></div><div class="px-4 py-5"><label class="text-xs font-medium" for="mobile-search">Buscar fala por termo</label><div class="mt-2 flex h-11 items-center gap-2 rounded-md border border-input px-3"><span class="text-muted-foreground">⌕</span><input id="mobile-search" value="tema pesquisado" readonly class="min-w-0 flex-1 bg-transparent text-base outline-none" /></div><div class="mt-2 flex gap-2 overflow-hidden"><span class="shrink-0 rounded-full border border-border px-2.5 py-1 text-xs">Ano</span><span class="shrink-0 rounded-full border border-border px-2.5 py-1 text-xs">Tema</span><span class="shrink-0 rounded-full border border-border px-2.5 py-1 text-xs">Município</span></div>
<article class="mt-5 rounded-xl border border-border p-4"><p class="text-xs text-muted-foreground">Data da fala · Pequeno Expediente · 12:34</p><p class="mt-2 text-sm leading-relaxed">… trecho da fala sobre o <mark class="rounded-sm bg-estimate-pending px-0.5 text-foreground">tema pesquisado</mark> …</p><div class="mt-3 flex gap-1.5"><span class="rounded-full bg-secondary px-2 py-0.5 text-xs">Tema da fala</span></div><a href="#" class="mt-3 flex h-11 w-full items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-white">Ver fala</a><div class="ml-3 mt-4 border-l border-border pl-3"><p class="mb-2 text-xs font-medium text-muted-foreground">1 corte desta fala</p><div class="rounded-lg bg-muted/55 p-3 ring-1 ring-border"><div class="flex flex-wrap items-center gap-2"><span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">Publicado</span><span class="text-xs text-muted-foreground">00:48</span></div><p class="mt-2 text-sm font-medium">Trecho sobre o tema pesquisado</p><a href="#" class="mt-3 flex h-10 items-center justify-center rounded-lg border border-border bg-white text-sm font-medium">Abrir corte</a></div></div></article><p class="mt-4 text-xs text-muted-foreground">1 fala encontrada</p></div><div class="grid h-16 grid-cols-5 border-t border-border bg-white text-[10px] text-muted-foreground"><div class="flex flex-col items-center justify-center">⌂<span>Início</span></div><div class="flex flex-col items-center justify-center">⌖<span>Municípios</span></div><div class="flex flex-col items-center justify-center">◉<span>Atualizações</span></div><div class="flex flex-col items-center justify-center">□<span>Agenda</span></div><div class="flex flex-col items-center justify-center font-medium text-primary">•••<span>Mais</span></div></div></article>
<article class="w-[390px] max-w-full overflow-hidden rounded-[18px] border border-border bg-white frame-shadow"><div class="flex h-14 items-center bg-primary px-4 text-white"><span class="mr-3">←</span><div><p class="text-sm font-semibold">Pequeno Expediente</p><p class="text-[11px] text-white/75">Data da fala</p></div></div><div class="px-4 py-5"><h3 class="text-lg font-medium">Pequeno Expediente</h3><p class="mt-1 text-xs text-muted-foreground">Data da fala · 12:34</p><div class="mt-4 flex aspect-video items-center justify-center rounded-xl bg-[#292524] text-white"><span class="rounded bg-black/45 px-2 py-1 text-[11px] font-medium">NEEDS ASSET — miniatura real</span></div><section class="mt-7 border-t border-border pt-5"><h2 class="text-lg font-semibold tracking-tight">Cortes desta fala</h2><div class="mt-4 space-y-3"><div class="rounded-xl border border-border p-4"><div class="flex items-center gap-2"><span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">Publicado</span><span class="text-xs text-muted-foreground">00:48</span></div><p class="mt-2 text-sm font-medium">Trecho selecionado da fala</p><a href="#" class="mt-3 flex h-11 w-full items-center justify-center rounded-lg border border-border text-sm font-medium">Abrir corte</a></div><div class="cut-row-dashed rounded-xl border border-border p-4"><div class="flex items-center gap-2"><span class="rounded-full border border-border px-2 py-0.5 text-xs font-medium">Processando</span><span class="text-xs text-muted-foreground">00:36</span></div><p class="mt-2 text-sm font-medium">Trecho em preparação</p><a href="#" class="mt-3 flex h-11 w-full items-center justify-center rounded-lg border border-border text-sm font-medium">Abrir corte</a></div></div></section></div></article>
</div>
</section>
<!-- Cena 7: biblioteca -->
<section aria-labelledby="scene-library">
<div class="mb-4"><p class="text-xs font-semibold text-primary">CENA 07</p><h2 id="scene-library" class="mt-1 text-xl font-semibold tracking-tight">Biblioteca de cortes · glimpse do destino</h2></div>
<div class="overflow-x-auto rounded-xl border border-border bg-white frame-shadow"><div class="flex min-h-[560px] min-w-[1180px]"><aside class="w-64 shrink-0 border-r border-sidebar-border bg-sidebar p-3"><div class="mb-5 flex h-10 items-center gap-2 px-2"><div class="flex size-8 items-center justify-center rounded-lg bg-primary text-sm font-bold text-white">S</div><div><p class="text-sm font-semibold">Solla</p><p class="text-xs text-muted-foreground">Campanha</p></div></div><div class="rounded-lg bg-sidebar-accent p-1 text-sm"><div class="flex h-9 items-center px-2 font-medium">Comunicação <span class="ml-auto">⌄</span></div><div class="ml-5 border-l border-sidebar-border pl-3"><div class="flex h-9 items-center rounded-md px-2 text-muted-foreground">Acervo de falas</div><div class="mt-1 flex h-9 items-center rounded-md bg-white px-2 font-medium text-primary ring-1 ring-border">Biblioteca de cortes</div></div></div></aside><div class="flex-1"><header class="flex h-16 items-center border-b border-border px-8"><div><p class="text-base font-semibold">Biblioteca de cortes</p><p class="text-xs text-muted-foreground">Cortes guardados para editar, baixar e republicar</p></div></header><div class="mx-auto max-w-[900px] px-8 py-8"><label class="text-xs font-medium" for="library-search">Buscar corte por título ou descrição</label><div class="mt-2 flex h-11 items-center gap-2 rounded-md border border-input px-3"><span class="text-muted-foreground">⌕</span><input id="library-search" class="w-full bg-transparent text-sm outline-none" placeholder="Buscar por título ou descrição…" /></div><div class="mt-6 space-y-3"><article class="rounded-xl border border-border p-4"><div class="flex items-center gap-2"><span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">Publicado</span><span class="text-xs text-muted-foreground">Criado em data</span></div><h3 class="mt-2 text-sm font-medium">Trecho selecionado da fala</h3><p class="mt-1.5 text-xs text-muted-foreground"><a href="#" class="underline underline-offset-4">Pequeno Expediente · data</a> · 00:48</p><div class="mt-3 flex gap-2"><a href="#" class="flex h-10 items-center rounded-lg border border-border px-3 text-sm font-medium">▶ Abrir corte</a><a href="#" class="flex h-10 items-center rounded-lg px-3 text-sm font-medium">Copiar link</a></div></article><article class="rounded-xl border border-dashed border-border p-4"><div class="flex items-center gap-2"><span class="rounded-full border border-border px-2 py-0.5 text-xs font-medium">Despublicado</span><span class="text-xs text-muted-foreground">Criado em data</span></div><h3 class="mt-2 text-sm font-medium">Declaração para redes</h3><p class="mt-1.5 text-xs text-muted-foreground"><a href="#" class="underline underline-offset-4">Grande Expediente · data</a> · 01:06</p><a href="#" class="mt-3 flex h-10 w-fit items-center rounded-lg border border-border px-3 text-sm font-medium">▶ Abrir corte</a></article></div><p class="mt-4 text-xs text-muted-foreground">2 cortes encontrados</p></div></div></div></div>
</section>
<footer class="border-t border-black/10 pt-6 text-xs leading-5 text-muted-foreground">
<p><strong class="text-foreground">NEEDS ASSET:</strong> miniaturas reais dos vídeos nas cenas de detalhe (desktop e mobile). O gate usa placeholders explícitos e não inventa imagem.</p>
</footer>
</main>
</body>
</html>

View file

@ -0,0 +1,109 @@
# Acervo: encontrar cortes — na navegação, na fala e na busca
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1084
Priority: P2
Impeccable: C — fluxo/chrome novo na vertical Comunicação (nav + seção na fala + resultados de busca)
Design UI: docs/plans/acervo-encontrar-cortes-ui-design.html
Appetite: ~1,5–2 dias eng; um outcome verificável — a assessoria reencontra um corte existente (pela navegação, pela fala de origem ou pela busca) e o reaproveita, sem duplicar.
Responsável: —
## Intenção
Quatro pedidos da assessoria são o mesmo job: **achar e reaproveitar um corte que já existe**. Uma tentativa anterior (C168/#1015) chegou a merged no repo, mas o dono testou o ambiente e **não encontrou a edição** — o que está de pé precisa ser conferido e o fluxo, tornado achável. Hoje, na prática, só se chega à biblioteca por um botão dentro do Acervo de falas, e nada na página da fala lembra que já se cortou dali, então a pessoa refaz o corte. Na busca do acervo, cortes simplesmente não existem. Esta entrega coloca os cortes no caminho: um subitem na navegação de "Comunicação", uma seção "Cortes desta fala" no detalhe da fala e os cortes aparecendo na busca do acervo. É descoberta e reuso; nada de novo modelo de dados.
## Persona e fluxo
- **Persona / contexto:** assessoria de comunicação (`communicator`, mais `coordinator`/`candidate`) na mesa, correndo para publicar; muitas vezes dias depois do corte ter sido feito por outra pessoa.
- **Job principal:** em uma tela, saber se aquela fala ou tema já rendeu corte e abrir para reusar/editar — sem recortar de novo.
- **Fluxo desejado:** abre "Comunicação" na barra, vê "Acervo de falas" e "Biblioteca de cortes"; busca um tema, vê a fala e, aninhado, os cortes já feitos dela; abre a fala e ali há a lista dos cortes com status e link; abre qualquer corte e edita/reusa.
- **Anti-goals de produto:** não criar um segundo cadastro de corte nem uma segunda lista concorrente com a busca; não virar editor de fala; não expor internals de corte em superfície pública.
### Esboço de fluxo (C)
```text
[Comunicação] ──┬─→ [Acervo de falas] ─→ [busca "reforma tributária"]
│ └─ [fala + cortes aninhados] ─→ [corte: editar/reusar]
└─→ [Biblioteca de cortes] ─→ [lista completa] ─→ [detalhe: editar/reusar/share]
[fala (detalhe)] ─→ seção "Cortes desta fala" ─┬─→ [corte existe] ─→ [abrir na biblioteca]
└─→ [vazio] ─→ [Selecionar trecho] (C166/C167)
```
### Design UI (C)
- Design UI (gate): `docs/plans/acervo-encontrar-cortes-ui-design.html`
## Objetivo e aceite
- "Comunicação" ganha subitens visíveis: **Acervo de falas** e **Biblioteca de cortes** — a biblioteca e a edição ficam a um clique de qualquer lugar da vertical.
- O detalhe da fala mostra os cortes já feitos **daquela** fala (título, status, duração, link para o detalhe do corte), com vazio honesto quando não há.
- A busca do acervo mostra os cortes como **subitens da fala de origem** no resultado; cortes não viram uma lista paralela e confusa.
- **Guardrails:** gate é `speechCatalog` (communicator/coordinator/candidate); `advisor`/`leader` negados fail-closed, sem regressão em nada do C168.
- Corte sempre pertence a uma fala; a fala pode ter sido excluída (FK `SET NULL`) e a UI degrada sem quebrar.
- Nenhum internals de corte (media/status cru/erro) vaza; `/corte/<id>` público intocado.
- Sem collection nova, sem `Consent`, sem migration.
## Dados (intenção)
- **Vou apresentar dados?** Sim, superfície neste item: **uma lista paginada** (a biblioteca de cortes, já do C168) e **um resultado de busca** (cortes aninhados no acervo de falas). **Sem KPI agregado** — não há decisão que peça contagem/gráfico de cortes.
- **Decisões desbloqueadas:** assessoria escolhe *reusar/editar* vs *cortar de novo* ao ver o corte existente na fala; escolhe *abrir a biblioteca* vs seguir navegando ao vê-la no chrome.
- **Forma:** *adiada ao plano de implementação* — aqui só restrição de produto: contagem honesta na lista ("N cortes"), sem percentual/taxa.
## Dados da decisão (literais)
- Rotas: acervo `/campanha/comunicacao/acervo`; biblioteca `/campanha/comunicacao/acervo/cortes`; detalhe do corte `/campanha/comunicacao/acervo/cortes/[id]`; detalhe da fala `/campanha/comunicacao/acervo/[id]`; pública `/corte/<id>` **intocada**.
- Rótulos de nav (pt-BR): pai **"Comunicação"**; subitens **"Acervo de falas"** e **"Biblioteca de cortes"**.
- Regra de aninhamento na busca: o corte aparece **como subitem indentado abaixo do item da fala de origem**, só quando a fala está no resultado e tem ≥1 corte; **não** existe grupo "Cortes" separado no v1.
- Cabeçalho da seção na fala: **"Cortes desta fala"**.
- Vazio na fala sem cortes: título **"Nenhum corte desta fala ainda"**, descrição **"Selecione um trecho no player para criar o primeiro corte."**, CTA **"Selecionar trecho"**.
- Texto da busca do acervo passa a valer também sobre cortes da fala; rótulo segue "Buscar fala por termo".
- Estado de cada corte na fala: badges `Publicado`/`Despublicado`/`Processando`/`Falhou` (mesmos do C168).
## Direção no codebase (hipótese)
- **Áreas prováveis:** nav em `src/components/campaign/shell/nav.ts` (`CampaignNavItem` hoje é plano — precisa de subitens/ativo); caminhos em `src/lib/campaignPaths.ts`; página da fala `src/app/(campaign)/campanha/(app)/comunicacao/acervo/[id]/page.tsx`; loader novo de cortes-por-fala em `src/utilities/speech/speechPageData.ts` (hoje só lista-tudo/detalhe em `speechCutPageData.ts` e `findSpeechCutForActor` em `speechCutData.ts`); resultado `SpeechResultList`/`speechViewModels.ts`; `src/components/campaign/speech/`.
- **Precedente a olhar:** C154 (nav própria do communicator + gate `speechCatalog`), C168 (biblioteca/detalhe/editora de título), C166/C167 (selecionar trecho/cortar), C170 (teto removido).
- **Risco de acoplamento:** não duplicar a lista de cortes — reusar `loadSpeechCutAcervoPageData`/view models do C168; a busca continua em `speech.searchText` (não criar `searchText` de corte agora); respeitar o lockdown `advisor`/`leader`.
## Dependências
- **C168/#1015** — tentativa anterior (biblioteca, detalhe, edição, kill switch) merged em `main`, mas o dono relata que **não encontra a edição**; validar a superfície no ambiente real antes de assumir que existe e, então, torná-la achável.
- **C154** (entregue) — gate `speechCatalog` e nav do communicator.
- **C166/C167/C170** (entregues) — de onde o corte nasce no detalhe da fala; o CTA de vazio aponta para o fluxo deles (suave).
## Fora de escopo
- Editar título/descrição em si — já é do C168; aqui só se chega à tela.
- Qualquer mudança no contrato público `/corte/<id>` (unlisted, `noindex`, sem transcript).
- Facetas/filtros de corte (ano, fala de origem, status) na biblioteca — C168 é só `q` + paginação.
- Corte em massa / edição em lote / deduplicação automática.
- Busca acento-insensível e índice para corte (gatilho do C168: ~5–10 mil cortes) — vira item próprio se o volume pedir.
- Corte criado de dentro da busca (cortar direto de um resultado).
## Rabbit holes de produto
- **Redesenhar toda a IA do shell.** Se alguém "só completar" a navegação, refaz a sidebar inteira. **Corte neste item:** só a árvore de "Comunicação" ganha subitens; o resto do `nav.ts` fica igual.
- **Editar o corte dentro da fala.** A tentação é editar título/descrição ali mesmo e criar um segundo editor. **Corte neste item:** a fala só **lista e linka**; a edição continua morando no detalhe (C168).
- **Unificar a busca dos dois acervos.** Virar uma query única com `searchText` de corte exige migration/índice. **Corte neste item:** busca continua por fala; corte só aninhado.
- **Segunda lista de cortes.** "Cortes encontrados" como grupo separado recria a duplicação que motivou o pedido. **Corte neste item:** só aninhamento sob a fala.
## Questões em aberto (produto)
- **Corte que casa por título/descrição mas cuja fala não casa?** **Opções:** A (não aparece no v1) | B (traz a fala de origem como bloco-resultado, com o corte aninhado) | C (grupo "Cortes" separado). **Recomendação:** B — mantém uma lista única e ainda responde ao pedido "na busca também apareça cortes". _(assumido — validar com produto)_
- **Nav: subitens expansíveis vs abas na home da Comunicação?** **Opções:** A (subitens no item "Comunicação") | B (tabs). **Recomendação:** A — pedido literal ("subitems dentro de Comunicação") e reaproveita o padrão de nav existente.
- **Manter o botão "Biblioteca de cortes" no topo do acervo?** **Opções:** A (manter como atalho redundante) | B (remover). **Recomendação:** A — custo zero e ajuda quem já tem o reflexo; o subitem é o caminho novo.
- **Status do corte aparece na busca?** **Opções:** A (só título + duração) | B (título + status). **Recomendação:** B — "reaproveitar" pressupõe saber se está publicado; sem custo relevante.
## Referências
- GitHub Issue #1084
- Design UI (gate): `docs/plans/acervo-encontrar-cortes-ui-design.html` (+ `docs/plans/acervo-encontrar-cortes-ui-design-assets/` se houver SVG próprio)
- `docs/changelog/2026-09-15-c168.md` (biblioteca/edição), `docs/changelog/2026-09-13-c154.md` (nav/gate do communicator)
- Rotas/utilitários: `src/components/campaign/shell/nav.ts`, `src/lib/campaignPaths.ts`, `src/utilities/speech/speechCutPageData.ts`, `src/utilities/speech/speechPageData.ts`, `src/utilities/speech/speechCutListUrl.ts`
- `AGENTS.md` — lockdown de papéis de campanha e "edite o dono, não gema um irmão"
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável ("reencontrar e reusar um corte" pelos três caminhos); (2) appetite ~1,5–2 dias, sem schema/migration; (3) persona + job + aceite testáveis; (4) direção no codebase é hipótese (aponta donos sem travar solução); (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,344 @@
<!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 — Acervo: pré-visualizar trecho</title>
<style type="text/tailwindcss">
@theme inline {
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--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-destructive: var(--destructive);
--color-border: var(--border);
--color-ring: var(--ring);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
}
[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;
--destructive: #b42318;
--border: rgb(28 25 23 / 12%);
--ring: #c51414;
--sidebar: #fafaf9;
--sidebar-foreground: #1c1917;
}
* { box-sizing: border-box; }
body { font-family: var(--font-sans); }
svg { stroke-linecap: round; stroke-linejoin: round; }
.btn {
@apply inline-flex min-h-10 shrink-0 items-center justify-center gap-1.5 rounded-lg border border-transparent px-2.5 text-sm font-medium whitespace-nowrap transition-colors;
}
.btn-primary { @apply bg-primary text-primary-foreground; }
.btn-outline { @apply border-border bg-background text-foreground; }
.btn-secondary { @apply bg-secondary text-secondary-foreground; }
.icon { @apply size-4 shrink-0 fill-none stroke-current stroke-2; }
.scene-label { @apply mb-3 flex items-center gap-2 text-xs font-medium text-muted-foreground; }
.scene-label::before { content: ''; @apply h-px w-6 bg-border; }
.frame { @apply overflow-hidden rounded-xl border border-border bg-background; }
.selection-card { @apply rounded-lg border border-border bg-muted/30 p-3; }
.video-placeholder {
background:
radial-gradient(circle at 50% 34%, rgb(255 255 255 / 9%), transparent 24%),
linear-gradient(145deg, #292524, #0c0a09 72%);
}
</style>
</head>
<body class="bg-[#f5f5f4] text-foreground antialiased">
<main class="mx-auto max-w-[1400px] px-4 py-8 sm:px-8 sm:py-10">
<header class="mb-8 max-w-3xl">
<p class="text-sm font-medium text-primary">C173 · Design de gate</p>
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Pré-visualizar o trecho selecionado</h1>
<p class="mt-2 text-sm leading-6 text-muted-foreground">
Assessoria de comunicação confere o intervalo no mesmo player antes de compartilhar ou cortar.
O controle pertence à seleção; a fileira de ações permanece intacta.
</p>
</header>
<!-- CENA 1: DESKTOP -->
<section aria-labelledby="desktop-title">
<div class="scene-label" id="desktop-title">Cena 01 · Desktop · viewport de referência 1280 px</div>
<div class="frame mx-auto w-full max-w-[1280px]">
<div class="grid min-h-[900px] grid-cols-[216px_1fr]">
<aside class="border-r border-border bg-sidebar p-4" aria-label="Navegação da campanha">
<div class="flex h-10 items-center gap-2 border-b border-border pb-4 text-sm font-semibold">
<span class="grid size-7 place-items-center rounded-lg bg-primary text-xs font-bold text-primary-foreground">S</span>
Campanha
</div>
<nav class="mt-5 space-y-1 text-sm">
<div class="rounded-lg px-3 py-2 text-muted-foreground">Visão geral</div>
<div class="rounded-lg px-3 py-2 text-muted-foreground">Municípios</div>
<div class="rounded-lg px-3 py-2 text-muted-foreground">Lideranças</div>
<div class="rounded-lg bg-secondary px-3 py-2 font-medium text-foreground">Comunicação</div>
<div class="ml-3 border-l border-border pl-3">
<div class="rounded-md px-2 py-2 font-medium text-primary">Acervo de falas</div>
</div>
</nav>
</aside>
<div>
<div class="flex h-14 items-center justify-between border-b border-border px-6">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
Comunicação
<svg class="icon size-3" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
<span class="text-foreground">Detalhe da fala</span>
</div>
<div class="grid size-8 place-items-center rounded-full bg-secondary text-xs font-semibold">AS</div>
</div>
<div class="mx-auto max-w-[930px] p-6">
<a class="inline-flex items-center gap-1 text-sm text-muted-foreground" href="#">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>
Voltar ao acervo
</a>
<div class="mt-4 flex items-start justify-between gap-6">
<div>
<h2 class="text-2xl font-semibold tracking-tight">Fala em sessão plenária</h2>
<p class="mt-1 text-sm text-muted-foreground">Transcrição e vídeo da fala · data da sessão</p>
</div>
<span class="inline-flex h-5 items-center rounded-full bg-secondary px-2 text-xs font-medium">Discurso</span>
</div>
<div class="mt-5 grid grid-cols-[minmax(0,1fr)_270px] gap-5">
<div>
<div class="video-placeholder relative aspect-video overflow-hidden rounded-lg border border-border text-white">
<div class="absolute left-3 top-3 rounded-md bg-black/55 px-2 py-1 text-[11px] font-medium">Vídeo da Câmara</div>
<div class="absolute inset-0 grid place-items-center">
<div class="text-center">
<div class="mx-auto grid size-12 place-items-center rounded-full bg-white/95 text-stone-950">
<svg class="size-5 fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>
</div>
<p class="mt-3 text-[11px] font-medium text-white/70">NEEDS ASSET — thumbnail da fala</p>
</div>
</div>
<div class="absolute inset-x-3 bottom-3">
<div class="h-1 rounded-full bg-white/25"><div class="h-1 w-[39%] rounded-full bg-primary"></div></div>
<div class="mt-2 flex justify-between text-[10px] tabular-nums text-white/80"><span>02:14</span><span>05:42</span></div>
</div>
</div>
<!-- active state in the primary desktop composition -->
<div class="selection-card mt-3">
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="text-xs font-medium text-primary">Trecho selecionado</span>
<span class="text-xs tabular-nums text-muted-foreground"><span class="text-foreground">01:42</span> → <span class="text-foreground">02:14</span> · 32 s</span>
</div>
<div class="relative mt-4 h-9 select-none">
<div class="absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted"></div>
<div class="absolute left-[30%] right-[58%] top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-primary"></div>
<div class="absolute left-[30%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div>
<div class="absolute left-[42%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div>
</div>
<div class="mt-1 flex justify-between text-[10px] tabular-nums text-muted-foreground"><span>00:00</span><span>01:42</span><span>02:14</span><span>05:42</span></div>
<div class="mt-3 flex flex-wrap items-end justify-between gap-3 border-t border-border pt-3">
<p class="max-w-[430px] text-[11px] leading-relaxed text-muted-foreground">Clique nas frases: o limite encaixa no segmento (ímã). Arraste as alças para o ajuste fino. Mínimo 5s · sem limite — até o fim da fala.</p>
<button class="btn btn-outline" type="button">
<svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>
Pré-visualizar trecho
</button>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2" aria-label="Ações existentes intactas">
<button class="btn btn-secondary" type="button">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="7" r="3"/><path d="m8.7 8.3 11.3 5.7M8.7 15.7 20 10"/><circle cx="6" cy="17" r="3"/></svg>
Selecionar trecho
</button>
<button class="btn btn-primary" type="button">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 3.5 20 9l-11 11H3v-6z"/><path d="m12 6 6 6"/></svg>
Cortar vídeo
</button>
<button class="btn btn-outline" type="button">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4"/></svg>
Compartilhar
</button>
<button class="btn btn-outline" type="button">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"/></svg>
Baixar MP4
</button>
<button class="btn btn-outline" type="button">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15 15 12 10 9z"/><rect x="3" y="6" width="18" height="12" rx="4"/></svg>
Abrir no YouTube
</button>
</div>
</div>
<aside class="rounded-xl border border-border p-4">
<h3 class="text-base font-medium">Transcrição</h3>
<p class="mt-1 text-xs text-muted-foreground">Clique nas frases para marcar o trecho.</p>
<ol class="mt-3 space-y-1">
<li class="flex gap-3 rounded-lg px-2 py-2 text-sm"><span class="w-10 shrink-0 text-xs tabular-nums text-muted-foreground">01:28</span><span class="leading-relaxed">Trecho anterior da fala para dar contexto.</span></li>
<li class="flex gap-3 rounded-lg border-l-2 border-primary bg-primary/10 px-2 py-2 text-sm"><span class="w-10 shrink-0 text-xs tabular-nums text-primary">01:42</span><span class="leading-relaxed">Frase selecionada que inicia o trecho.</span></li>
<li class="flex gap-3 rounded-lg border-l-2 border-primary bg-primary/10 px-2 py-2 text-sm"><span class="w-10 shrink-0 text-xs tabular-nums text-primary">01:57</span><span class="leading-relaxed">Continuação dentro do intervalo selecionado.</span></li>
<li class="flex gap-3 rounded-lg px-2 py-2 text-sm"><span class="w-10 shrink-0 text-xs tabular-nums text-muted-foreground">02:14</span><span class="leading-relaxed">Próxima frase, fora do corte.</span></li>
</ol>
</aside>
</div>
<div class="mt-7 border-t border-border pt-5">
<h3 class="text-base font-medium">Estados do controle no mesmo bloco de seleção</h3>
<p class="mt-1 text-xs text-muted-foreground">A ação não migra para a fileira de compartilhar, cortar ou baixar.</p>
<div class="mt-3 grid grid-cols-3 gap-3">
<div class="selection-card">
<div class="flex items-center justify-between gap-2"><span class="text-xs font-medium text-primary">Trecho selecionado</span><span class="text-[10px] font-medium text-muted-foreground">PARADO</span></div>
<p class="mt-1 text-xs tabular-nums text-foreground">01:42 → 02:14 · 32 s</p>
<button class="btn btn-outline mt-3 w-full" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>Pré-visualizar trecho</button>
</div>
<div class="selection-card ring-1 ring-primary/30">
<div class="flex items-center justify-between gap-2"><span class="text-xs font-medium text-primary">Trecho selecionado</span><span class="inline-flex h-5 items-center gap-1 rounded-full bg-primary/10 px-2 text-[10px] font-medium text-primary"><span class="size-1.5 rounded-full bg-primary"></span>REPRODUZINDO</span></div>
<p class="mt-1 text-xs tabular-nums text-foreground">01:42 → 02:14 · 32 s</p>
<button class="btn btn-outline mt-3 w-full border-primary text-primary" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="6" width="3.5" height="12" rx=".5"/><rect x="13.5" y="6" width="3.5" height="12" rx=".5"/></svg>Parar pré-visualização</button>
</div>
<div class="selection-card">
<div class="flex items-center justify-between gap-2"><span class="text-xs font-medium text-primary">Trecho selecionado</span><span class="text-[10px] font-medium text-muted-foreground">FIM VISÍVEL · 02:14</span></div>
<p class="mt-1 text-xs tabular-nums text-foreground">01:42 → 02:14 · 32 s</p>
<button class="btn btn-outline mt-3 w-full" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>Pré-visualizar trecho</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CENA 2: MOBILE -->
<section class="mt-10" aria-labelledby="mobile-title">
<div class="scene-label" id="mobile-title">Cena 02 · Mobile · viewport de referência 390 px</div>
<div class="frame mx-auto w-[390px] max-w-full">
<div class="flex h-14 items-center justify-between bg-primary px-4 text-primary-foreground">
<button class="grid size-10 place-items-center" type="button" aria-label="Abrir menu"><svg class="icon size-5" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16" /></svg></button>
<span class="text-sm font-semibold">Campanha</span>
<span class="grid size-9 place-items-center rounded-full bg-white/15 text-xs font-semibold">AS</span>
</div>
<div class="p-4">
<a class="inline-flex min-h-10 items-center gap-1 text-sm text-muted-foreground" href="#"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>Voltar ao acervo</a>
<h2 class="mt-2 text-xl font-semibold tracking-tight">Fala em sessão plenária</h2>
<p class="mt-1 text-sm text-muted-foreground">Transcrição e vídeo da fala</p>
<div class="video-placeholder relative mt-4 aspect-video overflow-hidden rounded-lg border border-border text-white">
<div class="absolute inset-0 grid place-items-center">
<div class="text-center">
<div class="mx-auto grid size-11 place-items-center rounded-full bg-white/95 text-stone-950"><svg class="size-5 fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg></div>
<p class="mt-2 text-[10px] font-medium text-white/70">NEEDS ASSET — thumbnail da fala</p>
</div>
</div>
<div class="absolute inset-x-3 bottom-2 h-1 rounded-full bg-white/25"><div class="h-1 w-[39%] rounded-full bg-primary"></div></div>
</div>
<div class="selection-card mt-3">
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-primary">Trecho selecionado</span>
<span class="text-xs tabular-nums text-muted-foreground"><span class="text-foreground">01:42</span> → <span class="text-foreground">02:14</span> · 32 s</span>
</div>
<div class="relative mt-3 h-11 select-none">
<div class="absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted"></div>
<div class="absolute left-[30%] right-[58%] top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-primary"></div>
<div class="absolute left-[30%] top-1/2 h-8 w-5 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div>
<div class="absolute left-[42%] top-1/2 h-8 w-5 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div>
</div>
<div class="flex justify-between text-[10px] tabular-nums text-muted-foreground"><span>00:00</span><span>01:42</span><span>02:14</span><span>05:42</span></div>
<p class="mt-2 text-[11px] leading-relaxed text-muted-foreground">Arraste as alças para o ajuste fino. Mínimo 5s · sem limite — até o fim da fala.</p>
<button class="btn btn-outline mt-3 h-11 w-full" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>Pré-visualizar trecho</button>
</div>
<div class="mt-3 grid grid-cols-2 gap-2">
<button class="btn btn-secondary h-11" type="button">Selecionar trecho</button>
<button class="btn btn-primary h-11" type="button">Cortar vídeo</button>
<button class="btn btn-outline h-11" type="button">Compartilhar</button>
<button class="btn btn-outline h-11" type="button">Baixar MP4</button>
<button class="btn btn-outline col-span-2 h-11" type="button">Abrir no YouTube</button>
</div>
</div>
</div>
</section>
<!-- CENA 3: YOUTUBE -->
<section class="mt-10" aria-labelledby="youtube-title">
<div class="scene-label" id="youtube-title">Cena 03 · YouTube · limitação explícita e saída honesta</div>
<div class="frame mx-auto max-w-[860px] p-5 sm:p-6">
<div class="grid gap-5 md:grid-cols-[1fr_300px]">
<div>
<div class="video-placeholder relative aspect-video overflow-hidden rounded-lg border border-border text-white">
<div class="absolute left-3 top-3 rounded-md bg-black/55 px-2 py-1 text-[11px] font-medium">YouTube</div>
<div class="absolute inset-0 grid place-items-center"><div class="text-center"><div class="mx-auto grid size-12 place-items-center rounded-xl bg-[#c51414] text-white"><svg class="size-6 fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 7 8 5-8 5z" /></svg></div><p class="mt-3 text-[11px] font-medium text-white/70">NEEDS ASSET — thumbnail da fala</p></div></div>
</div>
<div class="mt-3 rounded-lg border border-border bg-muted/40 px-4 py-3">
<div class="flex gap-2.5">
<svg class="icon mt-0.5 size-4 text-muted-foreground" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>
<p class="text-xs leading-5 text-foreground/90">No YouTube, a pré-visualização começa no início do trecho — parar no fim exige o vídeo da Câmara.</p>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button class="btn btn-outline" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>Assistir na Câmara</button>
<button class="btn btn-outline" type="button"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 15 15 12 10 9z"/><rect x="3" y="6" width="18" height="12" rx="4"/></svg>Abrir no YouTube</button>
</div>
</div>
</div>
<div class="selection-card self-start">
<span class="text-xs font-medium text-primary">Trecho selecionado</span>
<p class="mt-1 text-xs tabular-nums text-foreground">01:42 → 02:14 · 32 s</p>
<div class="relative mt-3 h-9"><div class="absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted"></div><div class="absolute left-[30%] right-[58%] top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-primary"></div><div class="absolute left-[30%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div><div class="absolute left-[42%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div></div>
<button class="btn btn-outline mt-3 min-h-10 w-full" type="button"><svg class="icon fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>Pré-visualizar trecho</button>
<p class="mt-2 text-[11px] leading-relaxed text-muted-foreground">Começa em 01:42. No YouTube, a parada no fim é manual.</p>
</div>
</div>
</div>
</section>
<!-- CENA 4: SEM SUPERFÍCIE TOCÁVEL -->
<section class="mt-10" aria-labelledby="unplayable-title">
<div class="scene-label" id="unplayable-title">Cena 04 · Sem superfície tocável · seleção preservada</div>
<div class="frame mx-auto max-w-[860px] p-5 sm:p-6">
<div class="grid gap-5 md:grid-cols-[1fr_300px]">
<div class="flex aspect-video flex-col items-center justify-center gap-2 rounded-lg border border-border bg-muted/40 px-6 text-center">
<svg class="icon size-8 text-muted-foreground" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 4 4 4-4 4 4 4-4M4 20 20 4"/></svg>
<p class="text-sm font-medium text-foreground/90">Vídeo indisponível neste momento.</p>
<p class="max-w-md text-xs leading-5 text-muted-foreground">Esta fala não tem vídeo da Câmara nem ponto de início disponível no YouTube. A seleção continua disponível para compartilhar ou cortar.</p>
</div>
<div>
<div class="selection-card">
<div class="flex items-center justify-between gap-2"><span class="text-xs font-medium text-primary">Trecho selecionado</span><span class="text-xs tabular-nums text-muted-foreground">32 s</span></div>
<p class="mt-1 text-xs tabular-nums text-foreground">01:42 → 02:14</p>
<div class="relative mt-3 h-9"><div class="absolute inset-x-0 top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted"></div><div class="absolute left-[30%] right-[58%] top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-primary"></div><div class="absolute left-[30%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div><div class="absolute left-[42%] top-1/2 h-7 w-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-primary bg-background"></div></div>
<p class="mt-2 text-[11px] leading-relaxed text-muted-foreground">A seleção permanece ajustável. Pré-visualização não oferecida nesta superfície.</p>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button class="btn btn-primary" type="button">Cortar vídeo</button>
<button class="btn btn-outline" type="button">Compartilhar</button>
</div>
</div>
</div>
</div>
</section>
<footer class="mt-10 border-t border-border pt-5 text-xs leading-5 text-muted-foreground">
Design de gate · The Field Desk · Inter, stone-white, hairlines e Mandate Red apenas para ação primária, seleção e foco.
</footer>
</main>
</body>
</html>

View file

@ -0,0 +1,104 @@
# Acervo: pré-visualizar no player o trecho selecionado
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1083
Priority: P2
Impeccable: B — encaixe no player do detalhe do acervo (controle de seleção)
Design UI: docs/plans/acervo-player-preview-do-trecho-ui-design.html
Appetite: ~1 dia eng; um outcome verificável — antes de cortar, a assessoria aperta play e vê exatamente o que entra no corte.
Responsável: —
## Intenção
A assessoria já marca [início, fim] no acervo (C166/C170), compartilha o link (C166) e corta o arquivo (C167). Mas entre marcar e cortar falta o gesto mais óbvio: **ver o trecho**. Hoje o play do player toca a fala inteira; para conferir se o intervalo escolhido começa e termina onde ela quer, ela precisa caçar os segundos no dedo. O pedido é direto: com um trecho selecionado, apertar play e ver só o intervalo — tocar de início e parar no fim — para decidir com os olhos antes de criar o corte. Não é editar nem pré-renderizar: é o mesmo player, com a janela selecionada.
## Persona e fluxo
- **Persona / contexto:** assessoria de comunicação (communicator, coordenação e candidatura) em `/campanha/comunicacao`, montando peça com prazo curto, na mesa ou no celular.
- **Job principal:** conferir no player que o trecho selecionado é exatamente o que entra no corte, antes de compartilhar ou cortar.
- **Fluxo desejado:** abre a fala → "Selecionar trecho" → ajusta [início, fim] (frases/alças) → aperta **"Pré-visualizar trecho"** → o player toca do início e para no fim → ajusta se algo entrou/saiu do ponto → segue para "Compartilhar" (C166) ou "Cortar vídeo" (C167) com confiança de que o intervalo é o certo.
- **Anti-goals de produto:** não é editor de vídeo (trim/export/render), não é segundo player, não promete controle que o embed do YouTube não dá; seleção/compartilhamento/corte intactos; sem `Consent`/collection/migration.
### Esboço de fluxo (B)
```text
[seleção ativa: início→fim] → "Pré-visualizar trecho" → player toca [início, fim] e para no fim
→ "certo, era isso" → [Compartilhar (C166) | Cortar vídeo (C167)]
→ "quase" → ajusta alças/frases → pré-visualiza de novo
[superfície sem controle do fim (YouTube)] → honesto: reposiciona no início; sem prometer parada automática
```
### Design UI (B)
- Design UI (gate): `docs/plans/acervo-player-preview-do-trecho-ui-design.html` — cenas: barra de seleção com o controle "Pré-visualizar trecho" (parado / tocando), estado chegou-ao-fim, aviso honesto no player do YouTube, fala sem superfície tocável, mobile (~390px) e desktop (~1280px).
## Objetivo e aceite
- Com trecho selecionado em superfície que o app controla (vídeo da Câmara), "Pré-visualizar trecho" toca somente [início, fim]: posiciona no início, reproduz e **para no fim**.
- Se a seleção mudar durante/após a pré-visualização, o player reflete o novo intervalo (não continua tocando o antigo); a pré-visualização nunca vaza para fora do trecho selecionado.
- No embed do YouTube, a postura é honesta: o app não promete parada automática no fim; a pré-visualização reposiciona no início do trecho (ou a UI oferece o vídeo da Câmara quando houver trecho gerado), com aviso claro e sem inventar controle que o embed não dá.
- Seleção/compartilhamento/corte intactos; clique na transcrição continua posicionando fora do modo de seleção e estendendo o trecho dentro dele (C166).
- **Guardrails:** gate do acervo (communicator/coordinator/candidate; advisor/leader negados fail-closed); crédito "Fonte: Câmara dos Deputados · CC BY 4.0"; `excerptTMs`/seleção exata verbatim; sem segundo player; sem render/export; sem `Consent`/collection/migration; URLs públicas intocadas.
## Dados (intenção)
- **Vou apresentar dados?** Não — a superfície é o player e o controle de seleção; tocar/parar é feedback de ação, não dado do acervo.
- **Decisões desbloqueadas:** a assessoria decide ajustar (ou não) o intervalo **antes** de criar o corte; a coordenação herda cortes já conferidos (menos retrabalho na biblioteca do C168).
- **Forma:** _adiada ao plano de implementação_ — restrição: sem KPI/telemetria de pré-visualização nesta fatia.
## Dados da decisão (literais)
- Controle (pt-BR), na barra/bloco de seleção: rótulo **"Pré-visualizar trecho"** (ícone play); enquanto toca, vira **"Parar pré-visualização"**.
- Comportamento no vídeo da Câmara: `currentTime = início` → play → ao alcançar `fim`, pausa e deixa o fim visível (play pronto).
- Aviso honesto do YouTube (pt-BR): **"No YouTube, a pré-visualização começa no início do trecho — parar no fim exige o vídeo da Câmara."** Quando a fala tem trecho gerado, oferecer o caminho da Câmara (reuso do C171) em vez de só avisar.
- Sem superfície tocável (sem VOD e sem offset do YouTube): a pré-visualização não é oferecida; a seleção permanece para compartilhar/cortar.
- Não alterar rótulos/limites vigentes da seleção: mínimo 5 s, sem teto (até o fim da fala), fileira de ações atual.
- Sem novas chaves de `Consent`, collections ou migrations; nada persistido no servidor.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/campaign/speech/SpeechDetailPlayer.tsx` (dono do playback: `videoRef`, `seekTo`, estados de seleção) e o bloco de seleção em `src/components/campaign/speech/SpeechExcerptControls.tsx`.
- **Precedente a olhar:** C166/C170 (`SpeechExcerptControls` só traduz ponteiro/tecla em geometria pura — não é lugar de lógica de player), C171 (troca de superfície e saídas do YouTube), seek já existente (`seekTo`) e o `onTimeUpdate` do `<video>`.
- **Risco de acoplamento:** C162/C166/C167/C170/C171 andam no mesmo arquivo — encaixe cirúrgico, sem reescrever quadrantes; C172 está na fila do mesmo `SpeechDetailPlayer.tsx` (serializar). Pins prováveis: `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts`.
## Dependências
- **Serializa com C172** — mesmo arquivo `SpeechDetailPlayer.tsx`; encaixar depois (ou antes, combinado) para não misturar diffs.
- Suave: C169 (resolver do corte) — só afeta o caminho "pré-visualizar pelo vídeo da Câmara" ser mais confiável; sem bloqueio.
- Entregues (satisfeitas): C162 (player), C166 (seleção/share), C167 (corte), C170 (sem teto), C171 (superfícies/saídas).
## Fora de escopo
- Editor de vídeo, timeline com zoom, precisão de frame, trim/export/render; preview renderizado do arquivo final.
- Loop contínuo do trecho, controle de velocidade, waveform, atalhos de teclado de scrub, "pular para o fim".
- Segundo player/segunda superfície de vídeo; baixar o trecho só para pré-visualizar.
- Alterar mínimo/máximo, ímã de frase, compartilhamento (C166), corte/publicação (C167/C168) e a biblioteca.
- `Consent`/collection/migration novas; ampliar papéis com acesso ao acervo; telemetria.
## Rabbit holes de produto
- **"Já que tem preview, faz loop e A/B do trecho."** Se alguém "só completar": loop, marcadores, comparação lado a lado. **Corte neste item:** um play que toca [início,fim] uma vez.
- **"Faz o preview ser um render do corte final."** Se alguém "só completar": fila, ffmpeg, storage, segundo artefato. **Corte neste item:** toca o próprio vídeo no navegador; nada é gerado.
- **"Usa a IFrame API do YouTube pra controlar o fim."** Se alguém "só completar": `enablejsapi`, postMessage, polling, estados condicionais. **Corte neste item:** no YouTube vale a postura honesta; o controle pleno fica no vídeo da Câmara.
- **"Aproveita e redesenha a barra de seleção."** Se alguém "só completar": re-layout, novos tokens, mexer no ímã. **Corte neste item:** encaixe cirúrgico do controle; seleção intocada.
## Questões em aberto (produto)
- **Como a pré-visualização se comporta no YouTube?** **Opções:** A) reposiciona no início e deixa parar manualmente, com aviso honesto | B) esconde o controle no YouTube e oferece "Assistir na Câmara" quando houver trecho | C) tenta o parâmetro `end` do embed e cai para A se não segurar. **Recomendação:** A como base, com o caminho da Câmara (C171) oferecido quando existir — nunca prometer parada que o embed não garante. _(assumido — validar com produto)_
- **Onde mora o controle?** **Opções:** A) no bloco de seleção, junto do intervalo | B) na fileira de ações do player. **Recomendação:** A — o controle pertence ao intervalo e some quando não há seleção; manter a lógica de playback no `SpeechDetailPlayer`, não na geometria pura. _(assumido)_
- **No fim do trecho, parar ou voltar ao início?** **Opções:** A) pausa no fim e mantém o play pronto | B) pausa e rebobina para o início da seleção. **Recomendação:** A — deixa o fim visível para conferência; rebobinar é gesto de replay, não pedido. _(assumido)_
- **Pré-visualizar exige o trecho já resolvido no vídeo da Câmara?** **Opções:** A) se o VOD ainda não foi resolvido, o play resolve e toca em seguida | B) pede "Assistir o trecho" primeiro. **Recomendação:** A — o gesto é único ("veja o trecho") e o resolver já é idempotente/verificado (C162/C171). _(assumido)_
## Referências
- GitHub Issue: #1083
- Design UI (gate): `docs/plans/acervo-player-preview-do-trecho-ui-design.html` (+ assets em `docs/plans/acervo-player-preview-do-trecho-ui-design-assets/` se houver)
- Planos irmãos: [`c166-compartilhar-trecho-link.md`](c166-compartilhar-trecho-link.md), [`c167-cortar-trecho-publicar.md`](c167-cortar-trecho-publicar.md), [`c170-corte-sem-limite.md`](c170-corte-sem-limite.md), [`c171-player-youtube-sem-signin.md`](c171-player-youtube-sem-signin.md)
- Arquivos-chave (pista, não contrato): `src/components/campaign/speech/SpeechDetailPlayer.tsx`, `src/components/campaign/speech/SpeechExcerptControls.tsx`, `src/lib/speechExcerptSelection.ts`, `src/components/campaign/speech/SpeechExcerptShare.tsx`, `src/components/campaign/speech/SpeechCutDialog.tsx`
- Testes a olhar: `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/unit/speechExcerptSelection.unit.spec.ts`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts`
- `AGENTS.md` — vertical Comunicação, gate do acervo e crédito CC BY.
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável (play toca [início,fim] e para no fim) sem superfície nova; (2) appetite de ~1 dia comporta o controle + a postura honesta do YouTube; (3) persona, job e aceite em linguagem de produto, com a copy literal separada; (4) direção no codebase é hipótese; (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,325 @@
<!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 C178 — player sem parede de signin</title>
<style type="text/tailwindcss">
@theme {
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--color-background: #ffffff;
--color-foreground: #1c1917;
--color-card: #ffffff;
--color-primary: #c51414;
--color-primary-foreground: #ffffff;
--color-secondary: #f5f5f4;
--color-secondary-foreground: #292524;
--color-muted: #f4f4f5;
--color-muted-foreground: #6b7280;
--color-border: rgb(28 25 23 / 12%);
--color-ring: #c51414;
--color-sidebar: #fafaf9;
--color-sidebar-foreground: #1c1917;
}
* { box-sizing: border-box; }
body { margin: 0; }
.scene-label {
@apply mb-3 flex items-center gap-2 text-xs font-medium text-muted-foreground;
}
.scene-label::before {
content: "";
@apply h-px w-5 bg-primary;
}
.btn {
@apply inline-flex min-h-10 shrink-0 items-center justify-center gap-1.5 rounded-lg border border-transparent px-3 text-sm font-medium whitespace-nowrap transition-all outline-none select-none active:translate-y-px;
}
.btn-primary {
@apply bg-primary text-primary-foreground hover:bg-primary/80 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50;
}
.btn-outline {
@apply border-border bg-background text-foreground hover:bg-muted focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50;
}
.btn-ghost {
@apply text-foreground hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50;
}
.poster-facade {
@apply relative flex aspect-video w-full cursor-pointer items-center justify-center overflow-hidden rounded-lg border bg-stone-900 outline-none transition-all;
}
.poster-facade::after {
content: "";
@apply pointer-events-none absolute inset-0 ring-0 ring-primary/50 ring-inset transition-all;
}
.poster-facade:hover::after,
.poster-facade:focus-visible::after,
.poster-facade.is-focus-demo::after {
@apply ring-4;
}
.poster-facade:hover .poster-action,
.poster-facade:focus-visible .poster-action,
.poster-facade.is-focus-demo .poster-action {
@apply scale-105 bg-white text-stone-950;
}
.poster-facade:focus-visible,
.poster-facade.is-focus-demo {
@apply ring-3 ring-ring/50;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
}
</style>
</head>
<body class="bg-stone-100 font-sans text-foreground antialiased">
<main class="mx-auto max-w-[1440px] px-4 py-10 sm:px-8 lg:px-12">
<header class="mx-auto mb-10 max-w-6xl">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-sm font-medium text-primary">C178 · Gate visual</p>
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Player sem parede de signin</h1>
<p class="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
Assessoria abre uma fala e encontra primeiro uma superfície controlável: o trecho da
Câmara na página ou uma capa honesta com saída externa em um clique.
</p>
</div>
<span class="rounded-full border border-border bg-white px-3 py-1 text-xs text-muted-foreground">
data-theme="campaign"
</span>
</div>
</header>
<!-- DESKTOP A -->
<section class="mx-auto mb-12 max-w-6xl" aria-labelledby="desktop-a-title">
<div class="scene-label"><span id="desktop-a-title">Desktop (~1280px), cena A — trecho da Câmara tocando na página</span></div>
<div class="overflow-hidden rounded-xl border border-border bg-background shadow-[0_4px_24px_rgb(28_25_23/0.08)]">
<div class="grid min-h-[760px] grid-cols-[210px_minmax(0,1fr)]">
<aside class="border-r border-border bg-sidebar p-3" aria-label="Navegação ilustrativa">
<div class="flex h-12 items-center gap-2 px-2">
<div class="grid size-8 place-items-center rounded-lg bg-primary text-sm font-bold text-white">S</div>
<span class="text-sm font-semibold">Campanha</span>
</div>
<nav class="mt-5 space-y-1 text-sm">
<div class="rounded-lg px-3 py-2 text-muted-foreground">Visão geral</div>
<div class="rounded-lg px-3 py-2 text-muted-foreground">Municípios</div>
<div class="rounded-lg px-3 py-2 text-muted-foreground">Pessoas</div>
<div class="rounded-lg bg-secondary px-3 py-2 font-medium text-foreground">Comunicação</div>
<div class="ml-4 rounded-lg bg-primary/8 px-3 py-2 font-medium text-primary">Acervo de falas</div>
</nav>
</aside>
<div class="min-w-0">
<header class="flex h-16 items-center border-b border-border px-7">
<div>
<p class="text-sm font-semibold">Discurso</p>
<p class="text-xs text-muted-foreground">Sessão plenária · data da fala</p>
</div>
</header>
<div class="p-7">
<button class="btn btn-ghost -ml-2" type="button" aria-label="Voltar ao acervo">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m15 18-6-6 6-6"/><path d="M9 12h10"/></svg>
Voltar ao acervo
</button>
<div class="mt-4 grid gap-6 lg:grid-cols-[minmax(0,1fr)_230px]">
<article class="min-w-0">
<h2 class="text-lg font-medium">Discurso</h2>
<p class="mt-1 text-xs text-muted-foreground">Sessão plenária · duração da fala</p>
<div class="mt-4 overflow-hidden rounded-lg border border-black/30 bg-black text-white">
<div class="relative aspect-video">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,#44403c_0%,#1c1917_46%,#09090b_100%)]"></div>
<div class="absolute inset-x-0 top-0 flex items-center justify-between p-4 text-xs text-white/75">
<span class="rounded-md bg-black/55 px-2 py-1">Trecho da Câmara</span>
<span>Fonte oficial</span>
</div>
<div class="absolute inset-0 grid place-items-center">
<div class="grid size-14 place-items-center rounded-full bg-white/12 ring-1 ring-white/25 backdrop-blur-sm">
<svg viewBox="0 0 24 24" class="ml-0.5 size-6" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/90 to-transparent px-4 pb-3 pt-9">
<div class="h-1 rounded-full bg-white/25"><div class="h-1 w-[38%] rounded-full bg-primary"></div></div>
<div class="mt-3 flex items-center gap-3 text-xs">
<svg viewBox="0 0 24 24" class="size-4" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<span class="tabular-nums">01:42 / 04:31</span>
<svg viewBox="0 0 24 24" class="ml-auto size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/></svg>
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
</div>
</div>
</div>
</div>
<div class="mt-3 rounded-lg border border-border bg-muted/40 px-4 py-3">
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-xs font-medium text-foreground/90">Se o vídeo não abrir aqui, assista por outro caminho:</p>
<p class="mt-1 text-xs leading-5 text-muted-foreground">O trecho da Câmara toca nesta página; o YouTube abre no ponto da fala.</p>
</div>
<a class="btn btn-primary" href="#desktop-a-title">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
Abrir no YouTube
</a>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<button class="btn btn-outline" type="button">Selecionar trecho</button>
<button class="btn btn-outline" type="button">Baixar vídeo (MP4)</button>
<button class="btn btn-outline" type="button">Abrir fonte</button>
</div>
<section class="mt-5" aria-label="Transcrição ilustrativa">
<p class="text-xs text-muted-foreground">Transcrição (clique para posicionar)</p>
<div class="mt-2 space-y-1">
<div class="flex gap-3 rounded-lg bg-muted px-2 py-2"><span class="w-12 text-xs tabular-nums text-primary">01:42</span><span class="text-sm text-foreground/90">Trecho da fala em reprodução nesta página.</span></div>
<div class="flex gap-3 rounded-lg px-2 py-2"><span class="w-12 text-xs tabular-nums text-muted-foreground">01:58</span><span class="text-sm text-foreground/90">A transcrição permanece disponível para posicionar o vídeo.</span></div>
</div>
</section>
</article>
<aside class="space-y-5 border-l border-border pl-5">
<section><h3 class="text-xs text-muted-foreground">Sumário oficial</h3><div class="mt-2 space-y-2"><div class="h-2.5 w-full rounded bg-muted"></div><div class="h-2.5 w-4/5 rounded bg-muted"></div><div class="h-2.5 w-2/3 rounded bg-muted"></div></div></section>
<section><h3 class="text-xs text-muted-foreground">Temas</h3><div class="mt-2 flex gap-1.5"><span class="rounded-full bg-secondary px-2 py-1 text-xs">Tema</span><span class="rounded-full bg-secondary px-2 py-1 text-xs">Tema</span></div></section>
<section class="border-t border-border pt-3"><p class="text-xs text-muted-foreground">Fonte: Câmara dos Deputados · CC BY 4.0</p></section>
</aside>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- DESKTOP B -->
<section class="mx-auto mb-12 max-w-6xl" aria-labelledby="desktop-b-title">
<div class="scene-label"><span id="desktop-b-title">Desktop, cena B — sem trecho da Câmara: capa clicável (hover/foco)</span></div>
<div class="rounded-xl border border-border bg-background p-7 shadow-[0_4px_24px_rgb(28_25_23/0.08)]">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_280px]">
<article class="min-w-0">
<h2 class="text-lg font-medium">Discurso</h2>
<p class="mt-1 text-xs text-muted-foreground">Sessão plenária · data da fala</p>
<button class="poster-facade is-focus-demo mt-4" type="button" aria-label="Abrir este trecho no YouTube">
<div class="absolute inset-0 bg-[linear-gradient(135deg,#292524_0%,#0c0a09_60%,#1c1917_100%)]"></div>
<div class="absolute inset-0 opacity-40 [background-image:linear-gradient(rgb(255_255_255/0.05)_1px,transparent_1px),linear-gradient(90deg,rgb(255_255_255/0.05)_1px,transparent_1px)] [background-size:32px_32px]"></div>
<div class="absolute inset-x-5 top-5 flex items-start justify-between gap-3 text-left">
<span class="rounded-md border border-white/20 bg-black/45 px-2 py-1 text-xs text-white/80">Vídeo no YouTube</span>
<span class="rounded-md border border-amber-200/40 bg-amber-50 px-2 py-1 text-[10px] font-semibold text-amber-900">NEEDS ASSET — capa real do vídeo</span>
</div>
<div class="poster-action relative grid size-16 place-items-center rounded-full bg-white/90 text-stone-950 shadow-lg transition-all">
<svg viewBox="0 0 24 24" class="ml-1 size-7" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
</div>
<div class="absolute inset-x-5 bottom-5 flex items-center justify-between gap-3 text-left">
<div><p class="text-sm font-medium text-white">Abrir no YouTube</p></div>
<span class="rounded-full bg-primary px-2.5 py-1 text-[10px] font-semibold text-white">Estado: foco / hover</span>
</div>
</button>
<div class="mt-3 rounded-lg border border-border bg-muted/40 px-4 py-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="text-xs font-medium text-foreground/90">Abrir no YouTube</p>
<a class="btn btn-primary" href="#desktop-b-title">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
Abrir no YouTube
</a>
</div>
</div>
<div class="mt-3 flex flex-wrap gap-2"><button class="btn btn-outline" type="button">Selecionar trecho</button><button class="btn btn-outline" type="button">Abrir fonte</button></div>
</article>
<aside class="flex flex-col gap-4">
<div class="rounded-lg border border-dashed border-primary/40 bg-primary/5 p-4">
<div class="flex items-start gap-2">
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0 text-primary" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
<div><p class="text-xs font-medium text-primary">Caminho removido</p><p class="mt-1 text-xs leading-5 text-muted-foreground">aqui ficava a parede de signin — removida do caminho</p></div>
</div>
<div class="mt-3 flex aspect-video items-center justify-center rounded-md border border-dashed border-border bg-white text-center">
<p class="max-w-[16rem] px-4 text-[11px] leading-5 text-muted-foreground">Nenhum iframe do YouTube é carregado automaticamente.</p>
</div>
</div>
<p class="text-xs text-muted-foreground">Fonte: Câmara dos Deputados · CC BY 4.0</p>
</aside>
</div>
</div>
</section>
<!-- MOBILE -->
<section class="mx-auto mb-12 max-w-6xl" aria-labelledby="mobile-title">
<div class="scene-label"><span id="mobile-title">Mobile (~390px) — cenas A e B</span></div>
<div class="grid items-start gap-8 lg:grid-cols-2">
<!-- MOBILE A -->
<div>
<p class="mb-2 text-xs font-medium text-muted-foreground">Cena A · trecho da Câmara tocando</p>
<div class="mx-auto w-full max-w-[390px] overflow-hidden rounded-[20px] border border-stone-300 bg-background shadow-[0_10px_36px_rgb(28_25_23/0.12)]">
<header class="flex min-h-14 items-center bg-primary px-4 text-white">
<div><p class="text-sm font-semibold">Discurso</p><p class="text-xs text-white/75">Sessão plenária</p></div>
<div class="ml-auto grid size-9 place-items-center rounded-lg bg-white/10" aria-hidden="true">•••</div>
</header>
<div class="p-4 pb-7">
<button class="btn btn-ghost -ml-2" type="button"><svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>Voltar ao acervo</button>
<h2 class="mt-3 text-lg font-medium">Discurso</h2>
<p class="mt-1 text-xs text-muted-foreground">Data da fala · duração</p>
<div class="relative mt-4 aspect-video overflow-hidden rounded-lg border border-black/30 bg-[radial-gradient(circle_at_50%_35%,#44403c_0%,#09090b_75%)] text-white">
<span class="absolute left-3 top-3 rounded bg-black/55 px-2 py-1 text-[10px]">Trecho da Câmara</span>
<div class="absolute inset-0 grid place-items-center"><div class="grid size-12 place-items-center rounded-full bg-white/15 ring-1 ring-white/25"><svg viewBox="0 0 24 24" class="ml-0.5 size-5" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></div></div>
<div class="absolute inset-x-3 bottom-3"><div class="h-1 rounded bg-white/25"><div class="h-full w-[38%] rounded bg-primary"></div></div><p class="mt-2 text-[10px] tabular-nums text-white/75">01:42 / 04:31</p></div>
</div>
<div class="mt-3 rounded-lg border border-border bg-muted/40 p-3">
<p class="text-xs font-medium">Se o vídeo não abrir aqui, assista por outro caminho:</p>
<p class="mt-1 text-xs leading-5 text-muted-foreground">O trecho da Câmara toca nesta página; o YouTube abre no ponto da fala.</p>
<a class="btn btn-primary mt-3 w-full" href="#mobile-title"><svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>Abrir no YouTube</a>
</div>
<div class="mt-3 grid grid-cols-2 gap-2"><button class="btn btn-outline w-full" type="button">Selecionar trecho</button><button class="btn btn-outline w-full" type="button">Baixar MP4</button></div>
<section class="mt-5"><p class="text-xs text-muted-foreground">Transcrição (clique para posicionar)</p><div class="mt-2 flex gap-3 rounded-lg bg-muted p-2"><span class="text-xs tabular-nums text-primary">01:42</span><span class="text-sm leading-5">Trecho da fala em reprodução.</span></div></section>
<p class="mt-5 border-t border-border pt-3 text-xs text-muted-foreground">Fonte: Câmara dos Deputados · CC BY 4.0</p>
</div>
</div>
</div>
<!-- MOBILE B -->
<div>
<p class="mb-2 text-xs font-medium text-muted-foreground">Cena B · capa clicável em foco</p>
<div class="mx-auto w-full max-w-[390px] overflow-hidden rounded-[20px] border border-stone-300 bg-background shadow-[0_10px_36px_rgb(28_25_23/0.12)]">
<header class="flex min-h-14 items-center bg-primary px-4 text-white"><div><p class="text-sm font-semibold">Discurso</p><p class="text-xs text-white/75">Sessão plenária</p></div><div class="ml-auto grid size-9 place-items-center rounded-lg bg-white/10" aria-hidden="true">•••</div></header>
<div class="p-4 pb-7">
<button class="btn btn-ghost -ml-2" type="button"><svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>Voltar ao acervo</button>
<h2 class="mt-3 text-lg font-medium">Discurso</h2><p class="mt-1 text-xs text-muted-foreground">Data da fala · duração</p>
<button class="poster-facade is-focus-demo mt-4" type="button" aria-label="Abrir este trecho no YouTube">
<div class="absolute inset-0 bg-[linear-gradient(135deg,#292524,#09090b)]"></div>
<span class="absolute left-3 top-3 rounded border border-amber-200/40 bg-amber-50 px-2 py-1 text-[9px] font-semibold text-amber-900">NEEDS ASSET — capa real do vídeo</span>
<div class="poster-action relative grid size-12 place-items-center rounded-full bg-white/90 text-stone-950 shadow-lg transition-all"><svg viewBox="0 0 24 24" class="ml-0.5 size-5" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></div>
<div class="absolute inset-x-3 bottom-3 text-left"><p class="text-xs font-medium text-white">Abrir no YouTube</p></div>
</button>
<div class="mt-3 rounded-lg border border-border bg-muted/40 p-3"><a class="btn btn-primary w-full" href="#mobile-title"><svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>Abrir no YouTube</a></div>
<div class="mt-4 rounded-lg border border-dashed border-primary/40 bg-primary/5 p-3"><p class="text-xs font-medium text-primary">aqui ficava a parede de signin — removida do caminho</p><p class="mt-1 text-[11px] leading-5 text-muted-foreground">Nenhum iframe é carregado automaticamente.</p></div>
<button class="btn btn-outline mt-3 w-full" type="button">Abrir fonte</button>
<p class="mt-5 border-t border-border pt-3 text-xs text-muted-foreground">Fonte: Câmara dos Deputados · CC BY 4.0</p>
</div>
</div>
</div>
</div>
</section>
<!-- LOADING -->
<section class="mx-auto max-w-6xl" aria-labelledby="loading-title">
<div class="scene-label"><span id="loading-title">Loading honest state — resolução do trecho</span></div>
<div class="rounded-xl border border-border bg-background p-6 shadow-[0_4px_24px_rgb(28_25_23/0.08)]">
<div class="mx-auto flex aspect-video w-full max-w-2xl flex-col items-center justify-center gap-2 rounded-lg border border-border bg-muted/40 px-6 text-center text-muted-foreground" role="status" aria-live="polite">
<svg viewBox="0 0 24 24" class="size-6 animate-spin text-muted-foreground motion-reduce:animate-none" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
<p class="text-sm font-medium text-foreground/90">Resolvendo o trecho na Câmara…</p>
<p class="max-w-md text-xs leading-5">Isso pode levar alguns segundos. Se demorar, tente novamente em instantes — a página não trava.</p>
</div>
<div class="mx-auto mt-3 flex max-w-2xl items-center justify-between gap-3 rounded-lg border border-border bg-muted/40 px-4 py-3">
<p class="text-xs text-muted-foreground">A saída externa continua visível durante a espera.</p>
<a class="btn btn-primary" href="#loading-title"><svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>Abrir no YouTube</a>
</div>
</div>
</section>
<footer class="mx-auto mt-10 max-w-6xl border-t border-stone-300 pt-5 text-xs leading-5 text-muted-foreground">
<p><strong class="text-foreground">NEEDS ASSET:</strong> capa/poster real do vídeo para a fachada das cenas B (desktop e mobile).</p>
<p class="mt-1">Referência visual do gate; sem lógica, fetch, dados reais ou iframe do YouTube.</p>
</footer>
</main>
</body>
</html>

View file

@ -0,0 +1,100 @@
# Acervo: o player do YouTube deixa de ser uma parede de signin
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1081
Priority: P1
Impeccable: B — encaixe na superfície do player do detalhe do acervo
Design UI: docs/plans/acervo-player-youtube-sem-parede-de-signin-ui-design.html
Appetite: ~0,5–1 dia eng; um outcome verificável — abrir uma fala com vídeo nunca termina numa parede de login do YouTube.
Responsável: —
## Intenção
O dono testou o ambiente depois da entrega anterior (C171/#1047) e o defeito **continua**: a fala com vídeo abre no player e pede "faça signin para exibir o vídeo"; clicar em signin abre outra janela (onde a pessoa já está logada) e nada muda na página — o beco de login persiste. A tentativa anterior apostou em trocar o host do embed (`youtube-nocookie` → `youtube.com`) e em oferecer um bloco de saída, mas a superfície embutida continua sendo a parede de login quem recebe o usuário primeiro. Este item assume que o embed anônimo do YouTube não é confiável para as lives da Câmara e resolve de verdade: a superfície que o app controla (o vídeo do trecho da Câmara, quando existe) passa a ser o caminho padrão, e o YouTube deixa de ser a porta de entrada embutida — vira a saída externa em um clique, no ponto exato. O resultado é simples: quem abre uma fala do acervo assiste ou vê, na hora, como assistir — nunca fica preso.
## Persona e fluxo
- **Persona / contexto:** assessoria de comunicação (`communicator`, coordenação e candidatura) em `/campanha/comunicacao`, produzindo peça com prazo curto, na mesa ou em campo.
- **Job principal:** assistir a fala na própria página, sem depender de login no YouTube e sem ficar presa num aviso de signin.
- **Fluxo desejado:** abre a fala → se há trecho da Câmara, o vídeo **toca ali** (superfície padrão) → se não há, vê uma capa clara com a ação "Abrir no YouTube" (que abre no ponto, na conta de quem já está logado) → transcrição/clique para posicionar seguem funcionando.
- **Anti-goals de produto:** autenticar no YouTube; hospedar/proxy de cookie de sessão; segundo player/editor; detectar/inferir a causa do bloqueio do iframe; novo `Consent`/collection/migration; expor o acervo fora de `/campanha`.
### Esboço de fluxo (B)
```text
[abre fala com vídeo] → há trecho da Câmara? ─ sim → [toca na página] → saídas ("Abrir no YouTube") visíveis
└ não → [capa + "Abrir no YouTube"] (1 clique, no ponto)
→ nunca um iframe que exige signin como porta de entrada
```
### Design UI (B)
- Design UI (gate): `docs/plans/acervo-player-youtube-sem-parede-de-signin-ui-design.html` — cenas desktop (~1280px) e mobile (~390px): (a) superfície padrão tocando o trecho da Câmara com a saída "Abrir no YouTube"; (b) sem trecho da Câmara: capa clicável + "Abrir no YouTube"; (c) o estado de carregamento honesto. **Nenhuma cena com a parede de signin do YouTube.**
## Objetivo e aceite
- Abrir uma fala com vídeo **nunca** apresenta a parede de signin do YouTube: o usuário vê uma superfície que toca na página, ou uma capa com a saída em um clique.
- Quando a fala tem trecho da Câmara (`vodResolvable`), essa é a superfície padrão e toca neste item (reuso do resolver verificado C162/C169/C171).
- O caminho externo "Abrir no YouTube" abre no ponto exato da fala (depende do C172 para o ponto).
- Verificado com evidência **no ambiente que o dono testa** (staging): a fala do relato (997) deixa de terminar em signin.
- **Guardrails:** gate do acervo (communicator/coordinator/candidate; advisor/leader negados fail-closed); crédito "Fonte: Câmara dos Deputados · CC BY 4.0"; sem `Consent`/collection/migration; URLs públicas intocadas; transcrição/posicionamento (C162) e seleção/corte (C166/C167) intactos; sem baixar/espelhar/transcodificar.
## Dados (intenção)
- **Vou apresentar dados?** Não — a superfície é player + saídas; nenhum KPI/gráfico novo.
- **Decisões desbloqueadas:** a assessoria decide assistir na página sem depender do login do YouTube; a coordenação decide, pelo uso, se manter o YouTube como link externo (não embutido).
- **Forma:** _adiada ao plano de implementação_ — o estado do player é feedback de ação, não dado de acervo.
## Dados da decisão (literais)
- **Superfície atual:** `surface` em `src/components/campaign/speech/SpeechDetailPlayer.tsx` nasce `'youtube'` quando há `youtubeVideoId`, senão `'vod'`; o embed é `https://www.youtube.com/embed/<id>?playsinline=1&rel=0&start=<segundos>`.
- **Evidência do beco:** o app não vê o erro interno do iframe (sem `onError`, sem `enablejsapi`) — a saída não pode depender de detectar o bloqueio; por isso a superfície segura é a que o app controla.
- **Precedentes reusáveis:** resolver do trecho da Câmara (`acervo/resolver-vod` + `speechVodResolver.ts`, C162/C169) e as saídas do C171 ("Abrir no YouTube" no ponto via `buildSpeechExcerptYoutubeUrl`).
- **Copy pt-BR das saídas (manter):** "Se o vídeo não abrir aqui, assista por outro caminho:" / "Abrir no YouTube" / "Assistir na Câmara"; capa: "Assistir o trecho" quando houver VOD.
- **Fala sem VOD e sem YouTube:** estado honesto "Vídeo indisponível neste momento." (mantém).
- Sem `Consent`/collection/migration; nada persistido no servidor.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/campaign/speech/SpeechDetailPlayer.tsx` (escolha da superfície padrão, `renderMedia`, bloco de saídas), `src/utilities/speech/speechVodResolver.ts` + rota `acervo/resolver-vod` (reuso), `src/lib/speechShare.ts` (link externo no ponto).
- **Precedente a olhar:** C171 (`acervo-vod-sob-demanda`/`c171-player-youtube-sem-signin`), `src/components/CampaignStorySection.tsx` (embed do site público — referência de postura, não de solução).
- **Risco de acoplamento:** C172 está na fila do mesmo `SpeechDetailPlayer.tsx` (o ponto do `t=`) — serializar; pins `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts` (o e2e aborta `youtube*` globalmente — não carrega o embed real).
## Dependências
- **C172** — o link externo precisa abrir no ponto certo; sem ele o item fica pela metade.
- **Serializa com C172 e C173** — mesmo arquivo `SpeechDetailPlayer.tsx`.
- Tentativas anteriores que **não** resolveram para o dono: C171/#1047, C162/#999 (validar no ambiente antes de assumir que a superfície atual existe e funciona).
## Fora de escopo
- Fazer o YouTube tocar quando ele bloqueia o embed (login/idade/região); OAuth/SSO no YouTube.
- Detectar/classificar o motivo do bloqueio no iframe.
- Baixar/espelhar/transcodificar; re-hospedar no S3 redesenhar lista/detalhe além do encaixe do player.
- Exposição pública do acervo; segundo player; novo `Consent`/collection/migration.
## Rabbit holes de produto
- **"Fazer o YouTube sempre tocar".** Se alguém "só completar": OAuth, player próprio, proxy de mídia. **Corte neste item:** a superfície padrão é a que controlamos; o YouTube vira saída externa.
- **Detectar o bloqueio no iframe.** Se alguém "só completar": IFrame API + eventos + polling + estados condicionais. **Corte neste item:** a solução não depende de detectar — o embed deixa de ser a porta de entrada.
- **"Login resolve".** Se alguém "só completar": round-trip de sessão para dentro do iframe (impossível com cookies de terceiros). **Corte:** o embed é anônimo; o login é problema do YouTube.
- **Reescrever o player inteiro.** **Corte:** encaixe cirúrgico; comportamento existente (transcrição, seleção, download, corte) intacto.
## Questões em aberto (produto)
- **Como eliminar a parede de signin?** **Opções:** A) superfície padrão = trecho da Câmara quando existir, com "Abrir no YouTube" como saída externa; B) manter o iframe, mas como capa clicável (click-to-load) que só carrega o YouTube se o usuário pedir; C) remover o embed e mostrar capa + "Abrir no YouTube" sempre. **Recomendação:** A + B (Câmara default quando há VOD; para o resto, capa clicável em vez de embed automático) — nenhuma superfície mostra a parede de login de cara. _(assumido — validar com produto)_
- **Onde fica "Abrir no YouTube"?** **Opções:** A) junto das ações, sempre visível | B) dentro da capa. **Recomendação:** A — saída em um clique, sem depender de detecção.
- **Fala sem trecho da Câmara e sem YouTube?** **Opções:** A) estado honesto "Vídeo indisponível neste momento." | B) esconder o bloco. **Recomendação:** A (mantém o comportamento atual).
## Referências
- GitHub Issue #1081
- Design UI (gate): `docs/plans/acervo-player-youtube-sem-parede-de-signin-ui-design.html`
- Planos irmãos: `docs/plans/c171-player-youtube-sem-signin.md` (tentativa anterior), `docs/plans/acervo-vod-sob-demanda.md` (C162), `docs/plans/c169-corte-vod-confiabilidade.md`
- Arquivos-chave (pista, não contrato): `src/components/campaign/speech/SpeechDetailPlayer.tsx`, `src/utilities/speech/speechVodResolver.ts`, `src/lib/speechShare.ts`, `tests/unit/speechDetailPlayer.unit.spec.tsx`, `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 (nunca uma parede de signin); (2) appetite ~0,5–1 dia comporta a troca de superfície e a capa, sem schema; (3) persona/job/aceite em linguagem de produto; (4) direção no codebase é hipótese com precedentes nomeados; (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,290 @@
<!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 — Acervo: preview do trecho na listagem de falas</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-border: var(--border);
--color-ring: var(--ring);
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-4xl: 9999px;
}
[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;
--border: rgb(28 25 23 / 12%);
--ring: #c51414;
}
* { box-sizing: border-box; }
body { font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
@media (prefers-reduced-motion: reduce) {
.animate-pulse { animation: none !important; }
}
</style>
</head>
<body class="min-h-screen bg-muted/60 text-foreground antialiased">
<main class="mx-auto flex max-w-[1440px] flex-col gap-10 px-4 py-8 sm:px-8">
<header class="max-w-3xl">
<p class="text-sm font-medium text-primary">Gate visual · C175</p>
<h1 class="mt-1 text-2xl font-semibold tracking-tight">
Acervo: preview do trecho na listagem de falas
</h1>
<p class="mt-2 text-sm leading-6 text-muted-foreground">
A assessoria reconhece a cena na varredura e decide qual fala abrir, sem transformar a
listagem em player.
</p>
</header>
<!-- DESKTOP -->
<section aria-labelledby="desktop-title">
<div class="mb-3 flex items-baseline justify-between gap-4">
<div>
<p class="text-xs font-medium text-primary">CENA 01</p>
<h2 id="desktop-title" class="text-base font-medium">Desktop · listagem do acervo</h2>
</div>
<span class="text-xs text-muted-foreground">Viewport de referência: 1280 px</span>
</div>
<div class="w-[1280px] max-w-full overflow-hidden rounded-xl border bg-background">
<div class="mx-auto max-w-5xl px-8 py-8">
<div class="mb-6 flex items-end justify-between gap-6 border-b pb-5">
<div>
<h3 class="text-2xl font-semibold tracking-tight">Acervo de falas</h3>
<p class="mt-1 text-sm text-muted-foreground">
Busque por assunto, tema, município ou palavra-chave.
</p>
</div>
<button class="min-h-11 rounded-lg border bg-background px-4 text-sm font-medium">
Biblioteca de cortes
</button>
</div>
<div class="mb-5 flex min-h-11 items-center rounded-md border border-stone-300 bg-background px-4 text-sm text-muted-foreground">
Buscar no acervo de falas
</div>
<div class="flex flex-col gap-3">
<!-- fala com vídeo -->
<article class="rounded-xl border bg-card p-4 text-card-foreground">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>Data da fala</span><span aria-hidden="true">·</span><span>Discurso</span
><span aria-hidden="true">·</span><span>Duração</span>
</div>
<div class="mt-2 flex items-start gap-4">
<a href="#" aria-label="Assistir no trecho" class="relative block h-20 w-32 shrink-0">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 480 360'%3E%3Crect width='480' height='360' fill='%23292524'/%3E%3Crect y='235' width='480' height='125' fill='%230c0a09'/%3E%3Ccircle cx='330' cy='126' r='42' fill='%2378716c'/%3E%3Cpath d='M275 280h110l-18-105h-74z' fill='%2357534e'/%3E%3C/svg%3E" alt="" class="h-20 w-32 rounded-lg border object-cover" />
<span class="absolute inset-x-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-1 text-center text-[9px] font-semibold tracking-wide text-white">
NEEDS ASSET
</span>
</a>
<p class="min-w-0 flex-1 text-sm leading-relaxed text-foreground/90">
… Trecho da fala com contexto suficiente para reconhecer a cena e o assunto
antes de abrir o conteúdo completo. …
</p>
</div>
<div class="mt-3 flex flex-wrap gap-1.5">
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Saúde</span>
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Bahia</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">SUS</span>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 3 14 9-14 9z" /></svg>
Assistir no trecho
</button>
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg px-4 text-sm font-medium">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
Abrir fonte
</button>
</div>
</article>
<!-- fala sem vídeo: sem reserva de mídia -->
<article class="rounded-xl border bg-card p-4 text-card-foreground">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>Data da fala</span><span aria-hidden="true">·</span><span>Questão de ordem</span
><span aria-hidden="true">·</span><span>Duração</span>
</div>
<p class="mt-2 text-sm leading-relaxed text-foreground/90">
… Fala sem vídeo: o excerto mantém a largura do card e nenhum espaço vazio de
mídia é reservado. …
</p>
<div class="mt-3 flex flex-wrap gap-1.5">
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Direitos</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">Câmara</span>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 3 14 9-14 9z" /></svg>
Ver fala
</button>
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg px-4 text-sm font-medium">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
Abrir fonte
</button>
</div>
</article>
<!-- busca com termo destacado -->
<article class="rounded-xl border bg-card p-4 text-card-foreground">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>Data da fala</span><span aria-hidden="true">·</span><span>Discurso</span
><span aria-hidden="true">·</span><span>Duração</span><span aria-hidden="true">·</span><span>Presidiu: Nome da presidência</span>
</div>
<div class="mt-2 flex items-start gap-4">
<a href="#" aria-label="Assistir no trecho" class="relative block h-20 w-32 shrink-0">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 480 360'%3E%3Crect width='480' height='360' fill='%23292524'/%3E%3Crect y='235' width='480' height='125' fill='%230c0a09'/%3E%3Ccircle cx='330' cy='126' r='42' fill='%2378716c'/%3E%3Cpath d='M275 280h110l-18-105h-74z' fill='%2357534e'/%3E%3C/svg%3E" alt="" class="h-20 w-32 rounded-lg border object-cover" />
<span class="absolute inset-x-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-1 text-center text-[9px] font-semibold tracking-wide text-white">
NEEDS ASSET
</span>
</a>
<p class="min-w-0 flex-1 text-sm leading-relaxed text-foreground/90">
… O fortalecimento da <mark class="rounded-sm bg-amber-200/70 px-0.5 text-foreground">saúde</mark>
pública exige presença nos municípios e defesa permanente do
<mark class="rounded-sm bg-amber-200/70 px-0.5 text-foreground">SUS</mark>. …
</p>
</div>
<div class="mt-3 flex flex-wrap gap-1.5">
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Saúde</span>
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Municípios</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">saúde pública</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">+2</span>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 3 14 9-14 9z" /></svg>
Assistir no trecho
</button>
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg px-4 text-sm font-medium">Abrir fonte</button>
</div>
</article>
</div>
</div>
</div>
</section>
<!-- MOBILE -->
<section aria-labelledby="mobile-title">
<div class="mb-3">
<p class="text-xs font-medium text-primary">CENA 02</p>
<h2 id="mobile-title" class="text-base font-medium">Mobile · card com empilhamento confortável</h2>
</div>
<div class="w-[390px] max-w-full overflow-hidden rounded-xl border bg-background">
<div class="border-b bg-primary px-4 py-3 text-primary-foreground">
<p class="text-sm font-semibold">Acervo de falas</p>
</div>
<div class="p-4">
<article class="rounded-xl border bg-card p-4 text-card-foreground">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>Data da fala</span><span aria-hidden="true">·</span><span>Discurso</span
><span aria-hidden="true">·</span><span>Duração</span>
</div>
<div class="mt-3 flex flex-col items-start gap-3">
<a href="#" aria-label="Assistir no trecho" class="relative block h-20 w-32">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 480 360'%3E%3Crect width='480' height='360' fill='%23292524'/%3E%3Crect y='235' width='480' height='125' fill='%230c0a09'/%3E%3Ccircle cx='330' cy='126' r='42' fill='%2378716c'/%3E%3Cpath d='M275 280h110l-18-105h-74z' fill='%2357534e'/%3E%3C/svg%3E" alt="" class="h-20 w-32 rounded-lg border object-cover" />
<span class="absolute inset-x-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-1 text-center text-[9px] font-semibold tracking-wide text-white">NEEDS ASSET</span>
</a>
<p class="text-sm leading-relaxed text-foreground/90">
… O fortalecimento da <mark class="rounded-sm bg-amber-200/70 px-0.5 text-foreground">saúde</mark>
pública exige presença nos municípios e defesa permanente do SUS. …
</p>
</div>
<div class="mt-3 flex flex-wrap gap-1.5">
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Saúde</span>
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Bahia</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">SUS</span>
</div>
<div class="mt-3 flex flex-col items-stretch gap-2 min-[380px]:flex-row min-[380px]:items-center">
<button class="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 3 14 9-14 9z" /></svg>
Assistir no trecho
</button>
<button class="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium">Abrir fonte</button>
</div>
</article>
</div>
</div>
</section>
<!-- LOADING -->
<section aria-labelledby="loading-title">
<div class="mb-3">
<p class="text-xs font-medium text-primary">CENA 03</p>
<h2 id="loading-title" class="text-base font-medium">Loading · slot da miniatura preservado</h2>
</div>
<div class="max-w-5xl rounded-xl border bg-background p-6">
<article class="rounded-xl border bg-card p-4 text-card-foreground">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>Data da fala</span><span aria-hidden="true">·</span><span>Discurso</span
><span aria-hidden="true">·</span><span>Duração</span>
</div>
<div class="mt-2 flex items-start gap-4">
<div aria-label="Carregando miniatura" class="h-20 w-32 shrink-0 animate-pulse rounded-lg border bg-muted"></div>
<p class="min-w-0 flex-1 text-sm leading-relaxed text-foreground/90">
… Trecho da fala com contexto suficiente para reconhecer a cena e o assunto antes
de abrir o conteúdo completo. …
</p>
</div>
<div class="mt-3 flex flex-wrap gap-1.5">
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-normal text-secondary-foreground">Saúde</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-normal text-muted-foreground">SUS</span>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 3 14 9-14 9z" /></svg>
Assistir no trecho
</button>
<button class="inline-flex min-h-11 items-center gap-2 rounded-lg px-4 text-sm font-medium">Abrir fonte</button>
</div>
</article>
</div>
</section>
<!-- ANOTAÇÃO DO ARTEFATO -->
<aside class="max-w-5xl rounded-xl border border-dashed bg-background p-4" aria-label="Anotação do design">
<p class="text-xs font-semibold text-foreground">Anotação do gate — não é copy do produto</p>
<ul class="mt-2 space-y-1 text-xs leading-5 text-muted-foreground">
<li><strong class="font-medium text-foreground">Miniatura escolhida:</strong> 128 × 80 px · <code>h-20 w-32 rounded-lg border object-cover</code>.</li>
<li><strong class="font-medium text-foreground">URL:</strong> <code>https://i.ytimg.com/vi/&lt;videoId&gt;/&lt;variante&gt;.jpg</code>.</li>
<li>Imagem estática; sem player, autoplay ou promessa de frame exato do trecho.</li>
</ul>
</aside>
</main>
</body>
</html>

View file

@ -0,0 +1,98 @@
# Acervo: preview do trecho na listagem de falas
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1085
Priority: P2
Impeccable: B — encaixe no card da lista do acervo
Design UI: docs/plans/acervo-preview-do-trecho-na-listagem-ui-design.html
Appetite: ~0,5–1 dia eng; um outcome verificável — quem varre a lista reconhece o trecho pela imagem, sem abrir fala por fala
Responsável: —
## Intenção
A assessoria varre o acervo de falas (`/campanha/comunicacao/acervo`) buscando o trecho que serve a uma peça, mas hoje a lista é só texto, data e chips — cada fala é uma caixa cinza igual à seguinte. O pedido veio direto: mostrar na listagem um preview do trecho da fala quando houver vídeo, de preferência do meio do trecho. Isso devolve reconhecimento visual à varredura: quem já viu o vídeo bate o olho e lembra; quem não viu ganha uma dica de cena antes de abrir. É um encaixe pequeno no card existente, não uma área nova.
## Persona e fluxo
- **Persona / contexto:** assessoria de comunicação (jornalista/videomaker), no escritório ou em campo, com prazo curto, varrendo resultados de busca para escolher a fala que vira card/áudio/vídeo.
- **Job principal:** reconhecer o trecho certo na lista e decidir se abre a fala, sem abrir fala por fala.
- **Fluxo desejado:** busca/filtra → resultados com miniatura do trecho ao lado do excerto → reconhece a cena → "Assistir no trecho" abre o detalhe já no timestamp.
- **Anti-goals de produto:** player na lista; autoplay; extração de frame no servidor; espelhamento de thumbnails; segundo cadastro/Consent; mexer na busca ou no excerto.
### Esboço de fluxo (B)
```text
[busca/filtros] → resultado do acervo (texto + chips + MINIATURA do trecho)
→ reconhece a cena pela imagem → clique em "Assistir no trecho" → [outcome: peça escolhida sem abrir fala por fala]
```
### Design UI (B)
- Design UI (gate): `docs/plans/acervo-preview-do-trecho-na-listagem-ui-design.html` — cenas desktop (1280) e mobile (~390): card com miniatura à esquerda do excerto, placeholder quando a fala não tem vídeo, e o estado da busca com destaque de termo.
## Objetivo e aceite
- Cada card da lista do acervo mostra uma miniatura do trecho quando a fala tem vídeo do YouTube; sem vídeo, o card segue como hoje (sem espaço vazio reservado indevidamente).
- A miniatura reflete a fala/trecho daquele resultado: quando há termo buscado, prioriza a fala do segmento casado; sem termo, a fala padrão do card.
- **Guardrail (limitação honesta):** não existe frame arbitrário "do meio do trecho" — o YouTube só oferece o conjunto padrão de thumbnails por vídeo. "Meio do trecho" é resolvido como a melhor miniatura disponível da fala, nunca um frame seekado. O plano de implementação escolhe a variante; a UI não promete precisão que o dado não tem.
- **Guardrail (performance):** imagens pequenas, carregamento preguiçoso, sem mudança de layout ao carregar; a lista não pode ficar mais lenta nem "pular".
- **Guardrail (LGPD/escopo):** nada novo de Consent; não expor nada além do que a fala já expõe na lista.
## Dados (intenção)
- **Vou apresentar dados?** Não — a miniatura não desbloqueia decisão numérica nem é métrica; é reconhecimento visual. Nenhum agregado novo nasce aqui.
- **Decisões desbloqueadas:** a assessoria escolhe qual fala abrir mais rápido, 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)
- **Forma da URL da miniatura:** `https://i.ytimg.com/vi/<videoId>/<variante>.jpg` — mesma família já usada em `SpeechCutResultCard.tsx` e `src/app/(frontend)/corte/[id]/page.tsx`.
- **Variantes candidatas:** `hqdefault.jpg` (480×360, sempre existe, com barras em vídeo 16:9) e `mqdefault.jpg` (320×180, enquadramento 16:9). Decisão final no plano de implementação; a UI usa `object-cover`.
- **Tamanho/estilo de referência:** `h-20 w-32 rounded-lg border object-cover` (precedente no card de cortes) — pequeno, sem dominar o card.
- **`videoId`:** obtido de `youtubeUrl` via `parseYoutubeVideoId` (`src/lib/speechVod.ts`).
- **Sem vídeo:** nenhuma imagem; placeholder neutro (ex.: ícone) ou card sem espaço de mídia — não inventar imagem.
- **Regra "meio do trecho":** usar a miniatura padrão do vídeo da fala; não há seek. Se houver múltiplas falas no card, usar a do segmento casado.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/utilities/speech/speechViewModels.ts` (novo campo de miniatura no `SpeechListItemViewModel`, reaproveitando `matchedSegment`/`pickMatchingSegment` e `parseYoutubeVideoId`), `src/components/campaign/speech/SpeechResultCard.tsx` (encaixe no layout), `src/lib/speechVod.ts` (reuso do parser de id).
- **Precedente a olhar:** `SpeechCutResultCard.tsx` (miniatura + texto), `src/app/(frontend)/corte/[id]/page.tsx`.
- **Risco de acoplamento:** a lista é usada por busca com destaque (`matchKind`) — não alterar o excerto/highlight; nada que exponha dado que o `communicator` já não vê; leader lockdown intocado.
## Dependências
- Nenhuma dura. Precedentes já entregues: C154 (view models da lista) e C162 (player/denormalização de `youtubeUrl`). Sem `youtubeUrl` no registro, o card simplesmente não mostra preview.
## Fora de escopo
- Player, autoplay ou hover-play na lista (destino: nenhum — anti-goal).
- Extração/render de frame no servidor, ffmpeg, fila de processamento (destino: nenhum).
- Espelhamento de thumbnails em S3/bucket (destino: nenhum).
- Miniatura do VOD da Câmara quando não há YouTube (avaliar depois de uso real).
- Mudança de busca, excerto, facetas ou ordenação.
## Rabbit holes de produto
- **"Extrair o frame exato do meio do trecho."** Se alguém "só completar": pipeline de ffmpeg, storage, transcodificação e jobs. **Corte neste item:** miniatura padrão do YouTube; o "meio do trecho" é a melhor miniatura disponível, declarado no aceite.
- **"Preview em vídeo no hover."** Se alguém "só completar": embed de player, autoplay, banda e ruído na varredura. **Corte neste item:** imagem estática.
- **"Backfill/baixar todas as capas."** Se alguém "só completar": mirroring, reconciliação e custo de storage. **Corte neste item:** hotlink da capa do YouTube.
## Questões em aberto (produto)
- **Qual variante de thumbnail?** **Opções:** A) `hqdefault` (sempre disponível, mesma do card de cortes) | B) `mqdefault` (16:9, mais leve). **Recomendação:** A — consistência com o que já existe; `object-cover` corta as barras. _(assumido — validar com produto)_
- **Onde a miniatura senta no card?** **Opções:** A) à esquerda do excerto, em linha (precedente dos cortes) | B) faixa no topo do card. **Recomendação:** A — mantém a altura do card e o ritmo de varredura.
- **A miniatura é clicável?** **Opções:** A) decorativa (o CTA "Assistir no trecho" segue dono do clique) | B) leva ao mesmo `watchHref`. **Recomendação:** B — alvo maior sem criar ação nova.
- **Mostrar quando não há termo casado?** **Opções:** A) sim, sempre que houver vídeo (recomendado) | B) só em `matchKind === 'segment'`. **Recomendação:** A — reconhecimento vale em toda varredura.
## Referências
- GitHub Issue: #1085
- Design UI (gate): `docs/plans/acervo-preview-do-trecho-na-listagem-ui-design.html`
- `src/components/campaign/speech/SpeechResultCard.tsx` · `SpeechResultList.tsx` · `src/utilities/speech/speechViewModels.ts` · `src/lib/speechVod.ts`
- `docs/plans/acervo-videos-comunicacao.md` (vertical e aceite do acervo) · `src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx`
- `AGENTS.md` — convenções de UI da campanha e `data-theme='campaign'`
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável (reconhecer o trecho pela imagem na varredura); (2) appetite ~0,5–1 dia, encaixe no card; (3) persona + job + aceite claros, com a limitação da miniatura declarada; (4) direção no codebase é hipótese; (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,84 @@
# Acervo: o player e o link abrem no ponto certo da fala
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1082
Priority: P1
Impeccable: A — N/A sem nova superfície (o defeito é de comportamento do seek, não de layout)
Design UI: N/A — sem UI
Appetite: ~0,5–1 dia eng; um outcome verificável — abrir uma fala (ou o link "Abrir no YouTube") posiciona o vídeo no mesmo ponto que o compartilhamento do próprio YouTube.
Responsável: —
## Intenção
A assessoria abre uma fala do Acervo no `/campanha/comunicacao/acervo/…` e o vídeo começa **fora do ponto** — sistematicamente alguns segundos à frente do começo real da fala. O mesmo acontece quando a pessoa clica em "Abrir no YouTube": o `t=` gerado não bate com o ponto que o próprio YouTube entrega ao compartilhar aquele trecho. Quem compartilha um trecho com a rede passa a mandar o público para alguns segundos depois do começo, e quem confere a fala na tela perde o início. A causa provável é que o `t=` do Acervo é calculado só pela distância `excerptTMs − eventStartAt`, sem descontar o atraso entre o relógio do `eventStartAt` e o início efetivo da sessão no vídeo — atraso que a vertical de relatório de cidade (C163) já mediu e desconta, mas o player/share do Acervo nunca aplicou. É um defeito de confiança: o número que a assessoria compartilha precisa conferir com o vídeo, não quase conferir.
## Persona e fluxo
- **Persona / contexto:** assessoria de comunicação na mesa, montando o recorte do dia e compartilhando trechos de fala por WhatsApp/YouTube; e qualquer pessoa que recebe o link e vai assistir.
- **Job principal:** abrir (na tela ou pelo link) exatamente o instante em que a fala começa, sem precisar adivinhar quantos segundos voltar.
- **Fluxo desejado:** entra na fala → o vídeo já começa no ponto certo → clica em "Abrir no YouTube"/compartilha → o `t=` do link leva ao mesmo ponto que o YouTube daria. Ninguém ajusta nada na mão.
- **Anti-goals de produto:** não virar "editor de offset por fala"; não expor um controle/configuração de lag para o usuário; não prometer precisão ao segundo quando a evidência não existir (melhor omitir `t=` do que inventar).
## Objetivo e aceite
- Abrir uma fala do Acervo posiciona o player no mesmo ponto de início da fala que a referência do YouTube entrega (o desvio deixa de ser sistemático).
- O link de "Abrir no YouTube"/compartilhamento carrega um `t=` que cai no mesmo ponto que o player — player e link não divergem.
- O desvio residual observável é pequeno e explicável por evidência (não "quase certo por sorte"); sem evidência, o comportamento é o de hoje/omitir, nunca um palpite.
- **Guardrails:** sem schema/Consent/migration; contratos de URL pública intactos; não mexer nos contratos de C162 (posição de clique no transcrito), C166 (seleção/trecho) e C167 (corte/publicação); quando o offset for desconhecido, **não** inventar `t=` (omitir); calibração **baseada em evidência**, não em chute.
## Dados (intenção)
- **Vou apresentar dados?** Não.
- **Por quê:** o item corrige comportamento de seek/compartilhamento; não produz, agrega nem exibe métrica, série, ranking ou mapa. Nenhuma superfície de números novos — o "dado" aqui é o literal do `t=` de um único link, tratado em Dados da decisão.
## Dados da decisão (literais)
- Exemplo do usuário (fala 997, staging): página `https://staging.jorgesolla1313.com.br/campanha/comunicacao/acervo/997`.
- Link que o app gera hoje: `https://www.youtube.com/watch?v=DC_i9Kp1LVk&t=11962s`.
- Link correto (compartilhamento do YouTube): `https://www.youtube.com/live/DC_i9Kp1LVk?si=WcQakSdJVE2CvW54&t=11949`.
- Desvio observado: ~13s (app **depois** do ponto real).
- Evidência da C163 (precedente, **não** copiar cegamente): sessão de 13/03/2018 começa ~**37s** depois do `eventStartAt`; `YOUTUBE_VIDEO_OFFSET_SECONDS = 37` em `scripts/lib/cityReportBlocks.mjs`; a fala 641 fechou `youtube.com/watch?v=2cX_gKkJH7Q&t=608s`.
- Nota de forma de URL (não é o outcome): o canônico de arquivo ao vivo é `/live/<id>?t=`; `watch?v=<id>&t=` também funciona — este item **não** troca a forma, só o ponto.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/lib/speechVod.ts` (dono do cálculo de offset), `src/lib/speechShare.ts:29-44` (builder do link), `src/utilities/speech/speechViewModels.ts:260` (onde `youtubeOffsetSeconds` nasce) e `src/components/campaign/speech/SpeechDetailPlayer.tsx:142-146,231-243` (start do embed e seek do transcrito).
- **Precedente a olhar:** `scripts/lib/cityReportBlocks.mjs` (C163 — como o relatório de cidade desconta o lag) e os changelogs `docs/changelog/2026-09-15-c163-falas-*.md`.
- **Risco de acoplamento:** o offset é calculado uma vez e consumido por player e share; se cada superfície aplicar sua própria correção, elas divergem. Respeitar C162/C166/C167 (posições de clique/seleção/corte seguem relativas ao clipe da Câmara, não ao relógio do YouTube).
## Dependências
- Evidência da C163 (`YOUTUBE_VIDEO_OFFSET_SECONDS = 37`) como precedente de método, não como valor a copiar.
- Contratos C162/C166/C167 a preservar.
## Fora de escopo
- Trocar a forma da URL para `/live/<id>?t=` (fica como nota; sem mudança de contrato de URL).
- Recuperar o `enablejsapi`/YouTube IFrame API para seek programático.
- Remapear os `startSeconds` do ASR do clipe da Câmara para o relógio da sessão.
- Calibração por fala/sessão com UI, tabela ou job de manutenção.
## Rabbit holes de produto
- **"É só somar uma constante fixa".** Se alguém só fixar um valor global: o desvio varia por sessão e o número "corrige" uma fala e erra outra. **Corte neste item:** validar contra mais de uma fala/sessão antes de travar qualquer valor.
- **"Vamos reconstruir o modelo de offsets".** Reescrever a matemática de relógio de parede/tempo de sessão puxa ASR, importação e C163. **Corte:** ajustar o ponto de consumo no dono atual, sem novo modelo.
- **"Oferece um controle de ajuste fino".** Dar slider de offset por fala transforma defeito em configuração e joga o erro no usuário. **Corte:** correção automática e, na dúvida, omitir `t=`.
## Questões em aberto (produto)
- **Como corrigir o ponto?** **Opções:** A) descontar o lag de início de sessão como a C163 (mesmo método, valor revalidado); B) fixar uma constante nova global; C) calibrar por sessão. **Recomendação:** A — usar o método da C163 no dono do Acervo, confirmando o valor com evidência datada da fala 997 (e da 641) antes de travar. _(assumido — validar com produto)_
- **Amostra mínima para considerar a evidência suficiente?** **Opções:** A) só a fala 997 do relato; B) 997 + 641 (a que a C163 já mediu). **Recomendação:** B — dois pontos com o mesmo comportamento dão confiança de que não é coincidência de uma sessão.
- **Corrigir player e link juntos?** **Opções:** A) só o link do YouTube; B) só o start do player; C) os dois pela mesma fonte de offset. **Recomendação:** C — divergir player e link reintroduz o defeito pela outra porta.
## Referências
- GitHub Issue #1082
- Design UI (gate): N/A — sem UI
- `docs/changelog/2026-09-15-c163-falas-calibracao-video.md`, `.../c163-falas-inicio-da-fala.md`, `.../c163-falas-ancora-trecho.md` — histórico de calibração (pista, não contrato)
- `src/lib/speechVod.ts`, `src/lib/speechShare.ts:29-44`, `src/utilities/speech/speechViewModels.ts:260`, `src/components/campaign/speech/SpeechDetailPlayer.tsx:142-146,231-243`, `scripts/lib/cityReportBlocks.mjs`
- `AGENTS.md` — contratos de URL pública e C162/C166/C167
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável (abrir fala/link cai no mesmo ponto do YouTube); (2) appetite ~0,5–1 dia, ajuste no dono; (3) persona/job/aceite em linguagem de produto; (4) direção no codebase é hipótese (arquivos como pista); (5) zero decisão dura de engenharia (sem schema/migration/signatures).

View file

@ -0,0 +1,364 @@
<!doctype html>
<html lang="pt-BR" data-theme="editorial">
<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 C176 — Página pública do corte</title>
<style type="text/tailwindcss">
@theme {
--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-border: var(--border);
--color-ring: var(--ring);
--color-site-header: var(--site-header);
--color-site-header-foreground: var(--site-header-foreground);
--color-site-header-muted: var(--site-header-muted);
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
}
:root,
[data-theme='editorial'] {
--background: #ffffff;
--foreground: #201b1a;
--card: #ffffff;
--card-foreground: #201b1a;
--primary: #c51414;
--primary-foreground: #ffffff;
--secondary: #f5f5f4;
--secondary-foreground: #292524;
--muted: #f4f4f5;
--muted-foreground: #6b7280;
--border: #e7e5e4;
--ring: #c51414;
--site-header: #ae1603;
--site-header-foreground: #fff8f2;
--site-header-muted: rgb(255 248 242 / 72%);
}
body {
background: #f5f5f4;
}
.scene-surface {
background: #ffffff;
}
.artifact-shadow {
box-shadow: 0 22px 54px rgb(28 25 23 / 10%);
}
.poster-grid {
background-color: #171717;
background-image:
radial-gradient(circle at 68% 28%, rgb(197 20 20 / 20%), transparent 27%),
linear-gradient(rgb(255 255 255 / 4%) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 4%) 1px, transparent 1px);
background-size: auto, 32px 32px, 32px 32px;
}
.focus-demo:focus-visible {
outline: 3px solid rgb(197 20 20 / 32%);
outline-offset: 2px;
}
</style>
</head>
<body class="min-h-screen font-sans text-foreground antialiased">
<main class="mx-auto max-w-[1440px] px-4 py-10 sm:px-8 lg:px-12">
<header class="mx-auto max-w-6xl">
<div class="flex flex-wrap items-end justify-between gap-5">
<div>
<p class="text-sm font-semibold text-primary">C176 · gate visual</p>
<h1 class="mt-2 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">
Página pública do corte
</h1>
<p class="mt-3 max-w-3xl text-base leading-7 text-stone-600">
Vídeo como herói, leitura confortável e compartilhamento WhatsApp-first para quem
recebe o link no celular.
</p>
</div>
<div class="rounded-lg border border-stone-300 bg-white px-3 py-2 text-xs font-medium text-stone-600">
Tema público · editorial neutro
</div>
</div>
<aside
class="mt-7 flex items-start gap-3 rounded-xl border border-stone-300 bg-white px-4 py-4 text-sm leading-6 text-stone-700"
aria-label="Decisão sobre a superfície"
>
<span class="mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded-full bg-stone-900 text-white">
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 8h.01M11 12h1v4h1" />
<circle cx="12" cy="12" r="9" />
</svg>
</span>
<p>
<strong class="text-stone-950">Superfície corrigida:</strong> a área que antes aparecia
tintada agora é branco neutro (<code class="font-mono text-xs">background/card</code>).
Não há faixa, card colorido nem gradiente atrás do título ou da descrição.
</p>
</aside>
</header>
<!-- SCENE 01: DESKTOP -->
<section class="mx-auto mt-14 max-w-[1280px]" aria-labelledby="desktop-label">
<div class="mb-4 flex flex-wrap items-center justify-between gap-2">
<div>
<p class="text-xs font-semibold text-primary">CENA 01</p>
<h2 id="desktop-label" class="mt-1 text-lg font-semibold text-stone-900">
Desktop · viewport de referência 1280 px
</h2>
</div>
<p class="text-xs text-stone-500">Publicado · com link opcional da sessão</p>
</div>
<article class="artifact-shadow scene-surface overflow-hidden rounded-xl ring-1 ring-stone-900/10">
<header class="sticky top-0 z-10 bg-site-header text-site-header-foreground">
<div class="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-8">
<span class="text-lg font-semibold tracking-tight">Jorge Solla</span>
<nav class="flex items-center gap-2 text-site-header-muted" aria-label="Redes sociais">
<span class="sr-only">Redes sociais</span>
<span class="inline-flex size-8 items-center justify-center rounded-lg" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.8">
<rect x="3" y="3" width="18" height="18" rx="5" /><circle cx="12" cy="12" r="4" /><path d="M17.5 6.5h.01" />
</svg>
</span>
<span class="inline-flex size-8 items-center justify-center rounded-lg" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M14 9.5 18.5 7c.7-.4 1.5.1 1.5.9v8.2c0 .8-.8 1.3-1.5.9L14 14.5z" /><rect x="3" y="6" width="11" height="12" rx="2" />
</svg>
</span>
<span class="inline-flex size-8 items-center justify-center rounded-lg" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3v-6h2l.4-3H15V9.5c0-.9.4-1.5 1.7-1.5H18V5.3c-.6-.1-1.5-.3-2.7-.3-2.7 0-4.3 1.6-4.3 4.3V11H8v3h3v6H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z" />
</svg>
</span>
</nav>
</div>
</header>
<main class="w-full bg-white">
<div class="mx-auto w-full max-w-4xl px-8 py-12">
<div class="poster-grid relative aspect-video w-full overflow-hidden rounded-xl border border-stone-800 bg-black text-white">
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<span class="mx-auto flex size-16 items-center justify-center rounded-full bg-white/95 text-stone-950 shadow-xl ring-1 ring-black/15">
<svg viewBox="0 0 24 24" class="ml-1 size-7" fill="currentColor" aria-hidden="true">
<path d="m8 5 11 7-11 7z" />
</svg>
</span>
<p class="mt-4 text-sm font-medium text-white/85">Reproduzir corte</p>
</div>
</div>
<div class="absolute top-4 left-4 rounded-md bg-black/70 px-2.5 py-1.5 text-[11px] font-semibold tracking-wide text-white ring-1 ring-white/15">
NEEDS ASSET · poster real do vídeo
</div>
<div class="absolute right-0 bottom-0 left-0 flex items-center gap-3 bg-gradient-to-t from-black/90 to-transparent px-4 pt-12 pb-3">
<span class="flex size-7 items-center justify-center" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-5" fill="currentColor"><path d="m8 5 11 7-11 7z" /></svg>
</span>
<div class="h-1 flex-1 rounded-full bg-white/30"><div class="h-1 w-0 rounded-full bg-white"></div></div>
<span class="text-xs tabular-nums text-white/90">0:00 / 2:14</span>
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m8 0h3a2 2 0 0 0 2-2v-3" /></svg>
</div>
</div>
<div class="mx-auto max-w-3xl">
<h3 class="mt-7 text-3xl font-bold leading-tight tracking-tight text-balance text-stone-950">
Título do corte publicado
</h3>
<div class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground">
<span>Discurso · 12/08/2026 · 2:14</span>
<span aria-hidden="true">·</span>
<a href="#" class="font-medium text-stone-700 underline decoration-stone-300 underline-offset-4 hover:text-primary">
Ver sessão no YouTube ↗
</a>
</div>
<p class="mt-5 max-w-[68ch] text-base leading-7 text-stone-700">
Descrição aprovada do corte. Este espaço resume a fala com contexto suficiente
para quem recebeu o link entender o trecho antes de compartilhar.
</p>
<div class="mt-7 flex flex-wrap items-center gap-2" aria-label="Ações de compartilhamento">
<a href="#" class="focus-demo inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:bg-primary/85">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M20 11.5a8 8 0 0 1-11.8 7L4 20l1.5-4.1A8 8 0 1 1 20 11.5Z" /><path d="M8.5 8.5c.4 3 2 4.6 5 5l1-1.2c.2-.3.6-.4.9-.2l2 1" /></svg>
Compartilhar no WhatsApp
</a>
<a href="#" class="focus-demo inline-flex h-11 items-center justify-center gap-2 rounded-lg border border-stone-300 bg-white px-4 text-sm font-medium text-stone-800 transition hover:bg-stone-50">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3" /></svg>
Copiar link
</a>
<a href="#" class="focus-demo inline-flex h-11 items-center justify-center gap-2 rounded-lg border border-stone-300 bg-white px-4 text-sm font-medium text-stone-800 transition hover:bg-stone-50">
<svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4" /><path d="M5 19h14" /></svg>
Baixar arquivo (MP4)
</a>
</div>
<footer class="mt-10 border-t border-border pt-4 text-xs leading-5 text-muted-foreground">
<p>Fonte: Câmara dos Deputados · CC BY 4.0</p>
<p class="mt-1">Página não listada — o link circula, mas não é indexada nem aparece em buscas.</p>
</footer>
</div>
</div>
</main>
</article>
<div class="mt-3 flex items-center gap-3 rounded-lg border border-dashed border-stone-400 bg-white px-4 py-3 text-xs leading-5 text-stone-600">
<span class="h-px w-10 bg-stone-500" aria-hidden="true"></span>
<p><strong class="text-stone-900">Hotspot da correção:</strong> todo o plano do player até o crédito é branco neutro; a antiga área tintada não existe mais.</p>
</div>
</section>
<!-- SCENE 02: MOBILE -->
<section class="mx-auto mt-20 max-w-6xl" aria-labelledby="mobile-label">
<div class="mb-4">
<p class="text-xs font-semibold text-primary">CENA 02</p>
<h2 id="mobile-label" class="mt-1 text-lg font-semibold text-stone-900">
Mobile · viewport de referência 390 px
</h2>
<p class="mt-1 text-xs text-stone-500">WhatsApp-first · ação primária acima da dobra</p>
</div>
<div class="flex flex-col items-start gap-6 lg:flex-row">
<article class="artifact-shadow scene-surface relative w-[390px] max-w-full overflow-hidden rounded-[28px] ring-1 ring-stone-900/15">
<header class="bg-site-header text-site-header-foreground">
<div class="flex h-14 items-center justify-between px-4">
<span class="text-lg font-semibold tracking-tight">Jorge Solla</span>
<div class="flex items-center gap-1 text-site-header-muted" aria-hidden="true">
<span class="flex size-9 items-center justify-center"><svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="5" /><circle cx="12" cy="12" r="4" /><path d="M17.5 6.5h.01" /></svg></span>
<span class="flex size-9 items-center justify-center"><svg viewBox="0 0 24 24" class="size-[18px]" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 9.5 18.5 7c.7-.4 1.5.1 1.5.9v8.2c0 .8-.8 1.3-1.5.9L14 14.5z" /><rect x="3" y="6" width="11" height="12" rx="2" /></svg></span>
</div>
</div>
</header>
<main class="bg-white pb-8">
<div class="poster-grid relative aspect-video w-full overflow-hidden bg-black text-white">
<span class="absolute top-3 left-3 rounded bg-black/75 px-2 py-1 text-[10px] font-semibold ring-1 ring-white/15">
NEEDS ASSET · poster real
</span>
<span class="absolute inset-0 m-auto flex size-14 items-center justify-center rounded-full bg-white/95 text-stone-950 shadow-xl">
<svg viewBox="0 0 24 24" class="ml-1 size-6" fill="currentColor" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>
</span>
<div class="absolute right-3 bottom-3 left-3 flex items-center gap-2 text-xs">
<svg viewBox="0 0 24 24" class="size-4" fill="currentColor" aria-hidden="true"><path d="m8 5 11 7-11 7z" /></svg>
<div class="h-1 flex-1 rounded-full bg-white/35"></div>
<span>0:00 / 2:14</span>
</div>
</div>
<div class="px-4">
<h3 class="mt-5 text-2xl font-bold leading-[1.2] tracking-tight text-balance text-stone-950">
Título do corte publicado
</h3>
<p class="mt-2 text-sm text-muted-foreground">Discurso · 12/08/2026 · 2:14</p>
<a href="#" class="focus-demo mt-5 inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground">
<svg viewBox="0 0 24 24" class="size-[19px]" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M20 11.5a8 8 0 0 1-11.8 7L4 20l1.5-4.1A8 8 0 1 1 20 11.5Z" /><path d="M8.5 8.5c.4 3 2 4.6 5 5l1-1.2c.2-.3.6-.4.9-.2l2 1" /></svg>
Compartilhar no WhatsApp
</a>
<p class="mt-6 max-w-[36ch] text-base leading-7 text-stone-700">
Descrição aprovada do corte. Este espaço resume a fala com contexto suficiente
para quem recebeu o link entender o trecho antes de compartilhar.
</p>
<div class="mt-5 grid grid-cols-2 gap-2" aria-label="Ações secundárias">
<a href="#" class="focus-demo inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-stone-300 bg-white px-3 text-center text-sm font-medium text-stone-800">
<svg viewBox="0 0 24 24" class="size-[17px] shrink-0" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3" /></svg>
Copiar link
</a>
<a href="#" class="focus-demo inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-stone-300 bg-white px-3 text-center text-sm font-medium leading-4 text-stone-800">
<svg viewBox="0 0 24 24" class="size-[17px] shrink-0" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4" /><path d="M5 19h14" /></svg>
Baixar arquivo (MP4)
</a>
</div>
<a href="#" class="mt-5 inline-flex text-sm font-medium text-stone-700 underline decoration-stone-300 underline-offset-4">
Ver sessão no YouTube ↗
</a>
<footer class="mt-8 border-t border-border pt-4 text-xs leading-5 text-muted-foreground">
<p>Fonte: Câmara dos Deputados · CC BY 4.0</p>
<p class="mt-1">Página não listada — o link circula, mas não é indexada nem aparece em buscas.</p>
</footer>
</div>
</main>
<div class="pointer-events-none absolute top-[812px] right-0 left-0 z-20 border-t border-dashed border-primary/60">
<span class="absolute right-3 -top-5 rounded bg-white px-2 text-[10px] font-semibold text-primary">dobra ~844 px</span>
</div>
</article>
<aside class="max-w-sm rounded-xl border border-stone-300 bg-white p-5 text-sm leading-6 text-stone-600">
<p class="font-semibold text-stone-950">Decisão mobile</p>
<p class="mt-2">
A ação primária ocupa a largura útil logo depois do título e da meta. Descrição e
ações secundárias vêm depois, preservando um único foco acima da dobra.
</p>
<div class="mt-4 flex items-center gap-2 text-xs font-medium text-stone-700">
<span class="size-2 rounded-full bg-primary"></span>
Um único CTA primário
</div>
</aside>
</div>
</section>
<!-- SCENE 03: UNAVAILABLE -->
<section class="mx-auto mt-20 max-w-[1280px]" aria-labelledby="unavailable-label">
<div class="mb-4">
<p class="text-xs font-semibold text-primary">CENA 03</p>
<h2 id="unavailable-label" class="mt-1 text-lg font-semibold text-stone-900">
Indisponível / despublicado
</h2>
<p class="mt-1 text-xs text-stone-500">Mesmo estado para id inexistente · não revela existência</p>
</div>
<article class="artifact-shadow scene-surface min-h-[520px] overflow-hidden rounded-xl ring-1 ring-stone-900/10">
<header class="bg-site-header text-site-header-foreground">
<div class="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-4 sm:px-6 lg:px-8">
<span class="text-lg font-semibold tracking-tight">Jorge Solla</span>
<div class="flex items-center gap-2 text-site-header-muted" aria-hidden="true">
<span class="size-4 rounded-full border border-current"></span>
<span class="size-4 rounded-full border border-current"></span>
<span class="size-4 rounded-full border border-current"></span>
</div>
</div>
</header>
<main class="flex min-h-[464px] items-center justify-center bg-white px-4 py-16">
<div class="max-w-lg text-center">
<span class="mx-auto flex size-12 items-center justify-center rounded-full bg-stone-100 text-stone-600">
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<circle cx="12" cy="12" r="9" /><path d="M8.8 8.8 15.2 15.2M15.2 8.8l-6.4 6.4" />
</svg>
</span>
<h3 class="mt-5 text-2xl font-bold leading-tight tracking-tight text-stone-950 sm:text-3xl">
Este corte não está disponível
</h3>
<p class="mx-auto mt-3 max-w-[48ch] text-base leading-7 text-stone-600">
O conteúdo pode ter sido removido ou o endereço não está correto.
</p>
<a href="#" class="focus-demo mt-6 inline-flex h-11 items-center justify-center rounded-lg border border-stone-300 bg-white px-4 text-sm font-medium text-stone-800 hover:bg-stone-50">
Voltar ao início
</a>
</div>
</main>
</article>
</section>
<footer class="mx-auto mt-14 max-w-6xl border-t border-stone-300 pt-6 text-xs leading-5 text-stone-500">
<p>
Gate C176 · rota e contrato unlisted/noindex preservados · sem transcrição, internals,
galeria pública ou player de YouTube.
</p>
</footer>
</main>
</body>
</html>

View file

@ -0,0 +1,103 @@
# Página pública do corte: revisão de design
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1086
Priority: P2
Impeccable: C — superfície pública existente redesenhada (a página `/corte/<id>`)
Design UI: docs/plans/corte-pagina-publica-design-ui-design.html
Appetite: ~1–1,5 dia eng; um outcome verificável — a página pública do corte passa a ser bonita e compartilhável, e o conteúdo não fica sobre um fundo colorido incômodo.
Responsável: —
## Intenção
"A página pública do corte não está bonita; revise o design da página pública de corte. Uma coisa que me incomodou foi o fundo colorido do conteúdo." O link `/corte/<id>` é a ponta do compartilhamento WhatsApp-first do acervo: é o que um cidadão abre no celular depois de receber o link. Hoje a página funciona (player, título, descrição, share) mas não parece uma peça cuidada — e o conteúdo parece assentar sobre uma superfície colorida que incomoda. Esta fatia redesenha SÓ essa página para que o vídeo seja o herói, a leitura seja confortável e o resultado dê orgulho de repassar, sem tocar no contrato (rota, unlisted/`noindex`, metadata). A origem do "fundo colorido" será investigada e eliminada como requisito explícito de design.
## Persona e fluxo
- **Persona / contexto:** quem recebe o link no WhatsApp — cidadão/eleitor no celular, com pressa e distração; secundariamente a assessoria que envia e quer um link apresentável.
- **Job principal:** assistir ao trecho e, se gostar, repassar ou baixar, percebendo na hora que é peça oficial do mandato.
- **Fluxo desejado:** abre o link → capa do player (vídeo como herói) → dá play e assiste → lê título, meta e descrição com conforto → "Compartilhar no WhatsApp" (primário) ou "Baixar arquivo (MP4)" → vê o crédito CC BY e a nota de não listagem.
- **Anti-goals de produto:** não vira galeria/índice público; não vira embed de YouTube; não expõe transcrição/internals; não promete o que a campanha não pode verificar; não muda rota nem indexação.
### Esboço de fluxo (C)
```text
[link no WhatsApp] → /corte/<id> (unlisted, noindex)
→ capa do player (herói) → play e assiste
→ título + meta (tipo · data · duração) + descrição
→ Compartilhar no WhatsApp (primário) · Copiar link · Baixar arquivo (MP4)
→ crédito CC BY + nota "página não listada"
[despublicado/id inexistente] → mesma tela "Este corte não está disponível"
```
### Design UI (C)
- Design UI (gate): `docs/plans/corte-pagina-publica-design-ui-design.html` — cenas desktop (~1280), mobile (~390) e indisponível/despublicado; no fundo do conteúdo, **nenhuma superfície tintada**.
## Objetivo e aceite
- A página lê como peça cuidada e compartilhável: o vídeo é o herói, com hierarquia clara título → meta → descrição → ações.
- O conteúdo textual assenta em superfície neutra do site público; **nenhum fundo colorido/tintado atrás do texto** (sem faixa, card colorido ou gradiente sob título/descrição).
- Compartilhar é WhatsApp-first e mobile-first: ação primária evidente acima da dobra no celular; download do MP4 ao lado.
- Leitura confortável: medida de prosa controlada, contraste AA, tamanho/entrelinha adequados no celular.
- Publicado (com e sem link de sessão no YouTube) e indisponível/despublicado — ambos coerentes e bonitos.
- **Guardrails:** unlisted/`noindex` preservado; rota `/corte/<id>` e contrato inalterados; sem transcrição/internals; crédito `Fonte: Câmara dos Deputados · CC BY 4.0` visível; sem galeria indexável.
## Dados (intenção)
- **Vou apresentar dados?** Não — superfície de assistir/ler/compartilhar; nenhum KPI/gráfico novo.
- **Decisões desbloqueadas:** quem recebe decide assistir/repassar; a assessoria decide se o link está apresentável para enviar. Sem decisão nomeável → nada a medir.
- **Forma:** N/A — sem dado agregado nem visualização a definir.
## Dados da decisão (literais)
- Copy pt-BR fixada: primário `Compartilhar no WhatsApp`; secundárias `Copiar link` e `Baixar arquivo (MP4)`; crédito `Fonte: Câmara dos Deputados · CC BY 4.0`; nota `Página não listada — o link circula, mas não é indexada nem aparece em buscas.`; link opcional `Ver sessão no YouTube ↗`.
- Título de indisponível: `Este corte não está disponível` (id inexistente e despublicado usam a MESMA tela — não revela existência).
- Meta linha: `<tipo> · <data da fala> · <duração>` (ex.: `Discurso · 12/08/2026 · 2:14`).
- Rota `/corte/<id>` inalterada; `robots: { index: false, follow: false }` inalterado.
- Requisito explícito: **zero fundo tintado atrás do conteúdo** — fundo = superfície neutra do site público (`background`/`card`, conforme `DESIGN.md`), jamais `scope`/`support-*`/`editorial-bg`/gradiente sob o texto.
- Fonte do vídeo: o MP4 armazenado (`media.url`) — nunca YouTube/VOD como player.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/app/(frontend)/corte/[id]/page.tsx`; `src/components/SpeechCutShareActions.tsx`; `src/components/SiteHeader.tsx`; superfícies/tokens públicos em `src/app/(frontend)/**`.
- **Precedente a olhar:** páginas públicas de artigo `[type]/[category]/[slug]/page.tsx` (superfície de prosa); `DESIGN.md` (themes `editorial`/`petition`, "Public header: Header Crimson").
- **Risco de acoplamento:** não tocar metadata/OG nem a resolução do id (C167); o card do acervo reusa `SpeechCutShareActions` (prop `primary`) — acabamento visual não pode quebrar esse uso.
## Dependências
- **C167 entregue** — a página pública e a rota já existem. Nenhuma dependência nova.
## Fora de escopo
- Qualquer mudança de rota, metadata/Open Graph, indexação ou contrato de compartilhamento (C167 congelado).
- Galeria/listagem pública de cortes, busca, perfil (unlisted por desenho).
- Transcrição, legendas, player custom, fundo alternativo de download.
- Redesign do header do site ou da paleta global — apenas a superfície desta página.
## Rabbit holes de produto
- **Redesign do site inteiro ("já que estou aqui").** Se alguém "só completar": mexer no `SiteHeader`, na home, na paleta. **Corte neste item:** só a página `/corte/<id>` (no máximo, o acabamento da share kit dentro dela).
- **Player custom/branded.** Se alguém "só completar": controles próprios, autoplay, capa animada. **Corte:** `<video controls>` com poster, só acabamento visual.
- **Inventar contexto/claims sobre a fala.** Se alguém "só completar": biografia, números, "assista e compartilhe" com promessa. **Corte:** título/descrição reais de C167 + meta mínima; nada não verificável.
## Questões em aberto (produto)
- **Quanto contexto da fala mostrar?** **Opções:** A) só o corte + crédito | B) meta mínima (tipo · data · duração) + link do YouTube quando houver | C) bloco de contexto maior. **Recomendação:** B — dá credibilidade e data sem virar verbete. _(assumido — validar com produto)_
- **Onde fica a share kit?** **Opções:** A) logo abaixo do player (acima da descrição) | B) abaixo da descrição | C) CTA fixo no rodapé mobile. **Recomendação:** A — WhatsApp primário visível acima da dobra mobile; sem CTA fixo nesta fatia.
- **Tratar a origem do "fundo colorido"?** **Opções:** A) só garantir superfície neutra no redesign | B) investigar e remover o token tintado que a produção aparenta usar. **Recomendação:** B fail-safe — investigar a origem (theme/`editorial-bg`/gradiente do body) e forçar superfície neutra local. _(assumido — validar com produto)_
## Referências
- GitHub Issue #1086
- Design UI (gate): `docs/plans/corte-pagina-publica-design-ui-design.html` (+ assets em `corte-pagina-publica-design-ui-design-assets/` se houver)
- `DESIGN.md` — "The Field Desk", themes `editorial`/`petition`, Signal Red Rule, header público.
- `AGENTS-public.md` — convenções do site público (corta quando toca `(frontend)`).
- Planos C167: `docs/plans/c167-cortar-trecho-publicar.md` e `...-impl.md` (rota, anti-goals, unlisted/noindex).
- Arquivo pivô: `src/app/(frontend)/corte/[id]/page.tsx`.
- Doutrina do design hi-fi: `.agents/skills/plan-issue/ui-design-html.md`.
## Self-score (shaping)
5/5 — (1) fatia = um outcome verificável (página bonita, compartilhável, sem fundo tintado); (2) appetite ~1–1,5 dia cabe com design hi-fi + port da página; (3) persona/job/aceite em linguagem de produto; (4) direção no codebase é hipótese; (5) zero decisão dura de engenharia.

View file

@ -0,0 +1,89 @@
# Launch do worktree honra as flags do humano: o modelo pedido e o `--auto` chegam onde deveriam
Status: rascunho
Atualizado em: 2026-09-16
Issue: #1088
Priority: P2
Impeccable: A — N/A (CLI/shell, sem superfície de produto)
Design UI: N/A — sem UI
Appetite: ~1 dia eng; um outcome verificável — o que a pessoa digita na linha (`--zen`, `--go`, `--auto`) é o que a sessão e a mensagem inicial usam, ou a flag falha alto.
Responsável: —
## Intenção
Quem opera o `pnpm worktree` no terminal digita a intenção inteira na própria linha: o modelo (`--zen`, `--go`, …) e o modo autônomo (`--auto`). Hoje o launcher engole parte disso sem avisar. Dois relatos, dois sintomas do mesmo defeito:
- "Rodei `worktree plan --zen` e ele não abriu o opencode com o Muse Spark 1.3 Free do OpenCode Zen. Como esperava."
- "Rodei `worktree next --go --auto`, e a mensagem inicial não foi lançada com `--auto`, como esperado."
A raiz é a mesma: o `worktree` registra o modelo escolhido, mas em `plan`/`new` (driverless por OPS119) ele nunca é aplicado à sessão — o TUI anexado cai no pin global (`deepseek/deepseek-flash`). E `--auto` não existe no vocabulário do launcher: é parseado como booleano desconhecido e descartado em silêncio, então a skill começa supervisionada quando o humano pediu autonomia. A promessa implícita do terminal — "o que eu digitei é o que roda" — está quebrada.
## Persona e fluxo
- **Persona / contexto:** dev no terminal, começando um run do opencode; escolheu o modelo de propósito (custo/capacidade) e, às vezes, quer o run autônomo até o PR.
- **Job principal:** abrir a sessão do worktree já no modelo e no modo (`--auto`) que digitou — ou ser avisado de que aquela flag não será honrada.
- **Fluxo desejado:** digita `worktree plan --zen` → cai no worktree e no TUI da sessão já em Muse Spark; digita `worktree next --go --auto` → o driver auto-submete `/work-issue --issue <N> --auto` e a skill segue sem pausa.
- **Anti-goals de produto:** não virar um seletor de modelo/TUI de configuração; não reinterpretar `--auto` do humano como o `--auto` de permissões do `opencode run`; não mexer no preset nem no mapa de modelos.
## Objetivo e aceite
- **Modelo honrado em TODOS os purposes.** `pnpm worktree <next|plan|new|fix> --<modelo>` abre a sessão **a que a pessoa anexa** no modelo pedido, inclusive nos driverless (`plan` sem bag, `new`), onde não há driver para aplicar `--model`.
- **`--auto` do humano chega à mensagem inicial.** Com `--auto` na linha, o driver auto-submete exatamente `/work-issue --issue <N> --auto` (next), `/plan-issue --auto <bag>` (plan com bag) e `/bug-fix --auto <bag>` (fix); sem `--auto`, as formas atuais (supervisionadas) permanecem.
- **Flag não honrada falha alto.** Flag desconhecida ou que o launcher não sabe honrar morre com mensagem clara (e sugestão de vizinho, quando houver) — nunca é descartada em silêncio.
- **Guardrails:** preset default `deepseek/deepseek-flash` intocado; OPS119 mantido (`plan`/`new` sem driver não auto-submetem skill); ciclo de vida OPS110 (`serve`/`start`/`attach`/`stop`, marcador `TEQO_WORKTREE_TERMINAL`) e o contrato `purposeInvocation` preservados; nenhuma mudança de provider/auth.
## Dados (intenção)
- **Vou apresentar dados?** Não.
- **Decisões desbloqueadas:** N/A — é comportamento de CLI, não superfície de dado; o efeito é observável no estado da sessão e no log do driver.
- **Forma:** _adiada ao plano de implementação_.
## Dados da decisão (literais)
- Mapa de modelos (não alterar valores): `zen -> opencode/muse-spark-1.3-contributor-free`; `go -> opencode-go/deepseek-v4.1-flash`; preset sem flag `deepseek/deepseek-flash`.
- Formas exatas do comando auto-submetido: `/work-issue --issue <N> --auto`; `/plan-issue --auto <bag>`; `/bug-fix --auto <bag>`.
- O modelo pedido deve constar na **sessão** (`model` no estado em `~/.local/state/teqo/agent-sessions/<slug>.json` e na sessão do servidor), não só no argv do driver.
- API do servidor: `POST /session` aceita `model: { id, providerID, variant? }` na criação (e há rota de troca de modelo da sessão).
## Direção no codebase (hipótese)
- **Áreas prováveis:** `scripts/worktree.mjs` (parser e `printLaunchDirective`), `scripts/lib/worktree.mjs` (`opencodeLaunchDirective`, `resolveWorktreeModel`), `scripts/lib/agent-session.mjs` (`purposeInvocation`, `driverArgs`, criação de sessão no CLI `scripts/agent-session.mjs`), `.agents/shell/worktree.sh`.
- **Precedente a olhar:** `validateSessionFlags` + `nearestAcceptedFlag` (OPS110-F1, `scripts/lib/agent-session.mjs`) é o padrão pronto de "flag desconhecida falha alto" a espelhar no `parseArgs` do worktree; `attachArgs`/`driverArgs` mostram onde o modelo entra (e não entra).
- **Risco de acoplamento:** o `opencode attach` não tem `--model`; aplicar o modelo só faz sentido via sessão (criação/rota de troca) ou via driver — o executor deve escolher o ponto que cobre o anexo, sem duplicar estado.
## Dependências
- OPS110 (sessões persistentes attach/detach) — entregue.
- OPS119 (`plan` sem bag driverless) — entregue; comportamento a preservar.
## Fora de escopo
- Variantes de modelo (`--variant`): continuam na config global do opencode via Ctrl+T (OPS95).
- Alterar o mapa `WORKTREE_MODEL_MAP` ou o preset default.
- Provider/auth, servidor compartilhado, painel OPS109 e o caminho headless do auto-unblock (`opencode run --auto`) — não é este defeito.
- Redesenho da semântica de `--auto` das skills (`plan-issue`/`work-issue`/`bug-fix`) — o contrato documentado é a fonte; aqui só o transporte.
## Rabbit holes de produto
- **"Já que mexo no modelo, faço um seletor no TUI".** Explode para UX de configuração e interage com Ctrl+T. **Corte neste item:** só passar o modelo que veio na linha.
- **"`--auto` também poderia ligar permissões".** Misturar o `--auto` de skill com o `--auto` do `opencode run` cria dois significados incompatíveis. **Corte:** `--auto` do humano = opt-out do GATE da skill; permissões seguem como hoje.
- **"Valido todas as flags do opencode command também".** O caminho `/worktree` (sem `TEQO_WORKTREE_TERMINAL`) tem escopo próprio. **Corte:** fail-high no parser do script, sem estender a varredura além dele.
## Questões em aberto (produto)
- **Como aplicar o modelo no caminho driverless?** **Opções:** A) driver mínimo só para fixar o modelo; B) criar a sessão com `model` (`POST /session` já aceita; o TUI anexado herda); C) aplicar só quando há driver (deixa `plan`/`new` quebrados). **Recomendação:** B — sem processo fantasma, sem mensagem espúria e sem reintroduzir auto-submit que a OPS119 removeu; se o TUI não herdar, cair para a rota de troca de modelo da sessão. _(assumido — validar na implementação)_
- **Onde `--auto` é anexado?** **Opções:** A) em `purposeInvocation`, junto do `--issue N`/bag; B) na diretiva de launch. **Recomendação:** A — é lá que o comando auto-submetido nasce, um único dono.
- **Rejeitar flag desconhecida ou só avisar?** **Opções:** A) falhar alto (padrão OPS110-F1); B) avisar e seguir. **Recomendação:** A — o defeito nasceu justamente do descarte silencioso.
## Referências
- GitHub Issue #1088
- Design UI (gate): N/A — sem UI
- Arquivos: `scripts/worktree.mjs`, `scripts/lib/worktree.mjs`, `scripts/lib/agent-session.mjs`, `scripts/agent-session.mjs`, `.agents/shell/worktree.sh`
- Testes que pinam os contratos: `tests/unit/worktree.unit.spec.ts`, `tests/unit/agentSession.unit.spec.ts`
- Doc das flags de skill (contrato do `--auto`): `.opencode/commands/plan-issue.md`, `.opencode/commands/work-issue.md`, `.opencode/commands/worktree.md`, `.agents/skills/worktree-next-issue/SKILL.md`
- `docs/AGENT-OPS.md` — ciclo de vida das sessões (OPS110/OPS119)
## Self-score (shaping)
4/5 — (1) fatia = um outcome verificável (as duas flags têm o efeito pedido ou falham alto); (2) appetite ~1 dia para parser + transporte + modelo na sessão; (3) persona no terminal e aceite em linguagem de uso; (4) direção no codebase é hipótese (áreas e precedente como pista); (5) pontos de injeção são questões abertas com recomendação.