mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
docs(C173): impl plan aprovado + extensão do artefato (trigger a)
This commit is contained in:
parent
b1beb07f2a
commit
45738b16f0
2 changed files with 559 additions and 0 deletions
126
docs/plans/acervo-player-preview-do-trecho-impl.md
Normal file
126
docs/plans/acervo-player-preview-do-trecho-impl.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Impl: Acervo: pré-visualizar no player o trecho selecionado
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-09-16
|
||||
Issue: #1083
|
||||
Intenção: docs/plans/acervo-player-preview-do-trecho.md
|
||||
Appetite restante: herdado (~1 dia eng; sem corte — sem schema/`Consent`/escrita; uma função pura em `src/lib` + o dono do playback + as duas suítes unit)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** com um trecho selecionado numa superfície que o app controla (vídeo da Câmara), apertar **"Pré-visualizar trecho"** posiciona no início, reproduz e **para no fim** — o mesmo player, com a janela selecionada; no YouTube a postura é honesta (reposiciona no início, avisa e oferece o caminho da Câmara via C171), nunca prometendo a parada que o embed não garante.
|
||||
- **O que NÃO negociar:** um único player (sem segundo player/superfície); a parada no fim só existe onde o app para o vídeo (`surface === 'vod'` com o `<video>` montado); mudar a seleção durante/após a pré-visualização **cancela** o preview e reflete o novo intervalo (nunca vaza para fora do trecho); seleção/compartilhamento/corte intactos; clique na transcrição segue posicionando fora do modo de seleção e estendendo dentro dele (C166); sem `Consent`/collection/migration; sem render/export; guardrails do acervo (gate `speechCatalog`, crédito CC BY, `excerptTMs` verbatim, URLs públicas) intocados.
|
||||
- **O que reavaliar:** (1) a hipótese de que o dono do playback decide onde o botão aparece — o dono é `SpeechDetailPlayer`, mas a **posição visual** é do artefato: o botão mora no card de seleção (`SpeechExcerptControls`), por slot, e o player passa o nó renderizado (decisão 1); (2) a questão em aberto do VOD não resolvido — **resolvida no GATE a favor da intenção (gesto único: resolve e toca)**; o plano original a cortava e foi corrigido, com estado visual novo a cargo do `designer` (trigger a); (3) `SpeechExcerptControls` permanece só-geometria: a lógica de tocar/parar não entra nele (recebe só slot + estado de apresentação).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S["trecho selecionado<br/>selection != null"] --> G{"estado da superfície?"}
|
||||
G -- "YouTube" --> Y["reposiciona o embed no início<br/>reuso do ramo YouTube de seekTo<br/>+ aviso honesto + Assistir na Câmara (C171)"]
|
||||
G -- "Câmara resolvida" --> P["video.currentTime = início<br/>play() · previewing = true"]
|
||||
G -- "Câmara a resolver (vodResolvable)" --> R["marca previewIntent<br/>requestResolution(false)"]
|
||||
R -- "resolved + playbackUrl" --> P
|
||||
R -- "generating / failed" --> F["limpa previewIntent<br/>StatusPanel assume"]
|
||||
P --> T{"onTimeUpdate<br/>currentTime >= fim?"}
|
||||
T -- "não" --> P
|
||||
T -- "sim" --> E["pause() · currentTime = fim<br/>fim visível · play pronto"]
|
||||
P -. "mudou a seleção (alças/frase/toggle)<br/>applySelection" .-> X["pause() · previewing = false<br/>previewIntent = null · novo intervalo refletido"]
|
||||
P -. "pause nativo (onPause)" .-> X
|
||||
Y --> Z["Compartilhar (C166) · Cortar (C167)<br/>intactos"]
|
||||
E --> Z
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) pré-visualização como estado do **dono do playback** (`SpeechDetailPlayer`), com o botão renderizado dentro do **card de seleção** (`SpeechExcerptControls`, por slot) e comportamento ramificado por superfície | B) lógica de mídia dentro de `SpeechExcerptControls` (o bloco de seleção vira dono do play) | C) componente/estado próprio do preview (terceiro irmão no arquivo).
|
||||
**Recomendação:** A — o `SpeechDetailPlayer` já é o dono de `videoRef`, `seekTo`, `surface`, `playbackUrl` e `onTimeUpdate`; o preview é só um modo de reprodução sobre o que já existe. O botão é renderizado pelo player e injetado no card pelo slot `previewArea` (posição do artefato: dentro do bloco de seleção), reusando `Button` e o `PlayIcon` (:3); o estado novo (`previewing`) é um booleano ao lado de `selection`/`surface`.
|
||||
**Rejeitadas:** B porque `SpeechExcerptControls` é geometria pura (só traduz ponteiro/tecla; `onChange` sobe a range) e passaria a carregar efeito de mídia — quebra a fronteira que o C166 estabeleceu e o que o explorer 7 confirma; C porque é o rabbit hole "segundo player/segunda superfície" que a intenção corta e duplicaria o dono do `<video>`.
|
||||
|
||||
### Decisões de engenharia
|
||||
|
||||
1. **Onde o controle ancora.** Opções: A) **dentro do card de seleção** (`SpeechExcerptControls`), no rodapé após a dica, via slot `previewArea?: ReactNode` + estado de apresentação `previewState` | B) na fileira de ações existente do `SpeechDetailPlayer` | C) barra de ações nova. **Recomendação:** A — o artefato de gate é explícito: o botão mora no `selection-card` e "A ação não migra para a fileira de compartilhar, cortar ou baixar" (`docs/plans/acervo-player-preview-do-trecho-ui-design.html:340`; cenas 1/2/3 desenham o botão dentro do card). O player continua **dono do playback** e passa o `Button` já renderizado como `ReactNode`; `SpeechExcerptControls` segue sem saber de mídia (recebe só o slot e um estado de apresentação `'idle' | 'playing' | 'ended'`) — a fronteira do C166 é preservada porque nenhum efeito de mídia entra no controle. **Rejeitadas:** B (contraria o artefato aprovado — a estrutura visual é do `designer`, o implementador porta classe-a-classe, não realoca); C (estrutura nova, anti-goal). _Posição resolvida pelo artefato — não vai ao gate como escolha._
|
||||
2. **YouTube: o que acontece.** Opções: A) **só** o aviso honesto + "Assistir na Câmara" (C171), sem botão de preview | B) botão com o mesmo rótulo **"Pré-visualizar trecho"** que apenas **reposiciona** o embed no início (reusa o ramo YouTube de `seekTo`, :231-238), nunca vira "Parar pré-visualização", com o aviso honesto sempre visível | C) tentar `end`/IFrame API no embed. **Recomendação:** B — porta a cena 3 (o artefato desenha o botão no card + a legenda "Começa em 01:42. No YouTube, a parada no fim é manual.") e o gesto é honesto porque só reposiciona; a **parada** (toggle "Parar pré-visualização" e pausa no fim) fica estritamente gated a `surface === 'vod'` com o `<video>` montado. **Rejeitadas:** A (o artefato desenha o botão; e reposicionar é útil e custa zero mecanismo novo); C (rabbit hole declarado na intenção — bot-check/sem API).
|
||||
3. **Pré-visualizar com o VOD ainda não resolvido (produto: gesto único).** Opções: A) **gesto único** — o controle aparece no quadrante "Assistir o trecho" (fala `vodResolvable`, sem `playbackUrl`) e, ao clicar, resolve o VOD e toca assim que o arquivo verificado chega | B) o controle só aparece com o `<video>` já montado (o assessor precisa clicar "Assistir o trecho" antes) | C) pede "Assistir o trecho" explicitamente. **Recomendação:** A — **decidido com o humano no GATE**, seguindo a recomendação da intenção: o resolver é idempotente/verificado (C162/C171) e o gesto é único ("veja o trecho"). **Estado novo:** `previewIntentRef` + effect que, quando `resolution.kind === 'resolved' && playbackUrl` e há intenção pendente, inicia o preview (seek ao início + `play()`) depois de o `<video>` montar; `generating`/`failed` limpam a intenção e o `StatusPanel`/`inlineNotice` assumem; o botão mostra o estado "resolvendo" enquanto a resolução corre. **Rejeitadas:** B (o plano original; contraria a recomendação da intenção e cria dois gestos); C (copy/estado redundante com o painel que já existe). **Design:** é um estado visual que o artefato não cobre → **trigger (a): o `designer` estende o artefato antes de o markup mudar**.
|
||||
4. **Parada manual vs. fim.** Opções: A) no fim, **pausa e deixa o fim visível** (play pronto); parar manualmente pausa onde está | B) pausa e rebobina ao início da seleção. **Recomendação:** A — replica a questão em aberto da intenção: o fim fica visível para conferência e o replay é o próximo clique; rebobinar automático é gesto de replay não pedido. **Rejeitada:** B.
|
||||
5. **Onde mora a matemática "chegou ao fim".** Opções: A) função pura em `src/lib/speechExcerptSelection.ts` (`excerptPreviewStopAt(range, currentSeconds): number | null`) consumida pelo `onTimeUpdate` | B) `if (current >= selection.endSeconds)` inline no componente | C) helper novo em `src/utilities/speech`. **Recomendação:** A — mantém o `onTimeUpdate` magro, é testável sem DOM (a suíte pura já existe) e é o dono natural da range; devolve o alvo exato de parada (`endSeconds`) para **clamp do overshoot** do `timeupdate` (~4 Hz). **Rejeitadas:** B (regra espalhada, sem teste puro; o brief pede a lib); C (client component importaria `utilities` por nada — `lib/` é client-safe por definição).
|
||||
6. **Mudança de seleção durante o preview.** Opções: A) um único `applySelection(next)` no player que **sempre** para o preview e então aplica a nova range; todos os call-sites (`onChange`, `onSegmentActivate`, `toggleSelection`) passam por ele | B) parar o preview em cada call-site separadamente. **Recomendação:** A — um caminho só garante o invariante "nunca vaza"; B esquece um call-site a cada novo modo. **Rejeitada:** B.
|
||||
7. **Anti-leak em pausa/scrub nativos.** Opções: A) `onPause` fecha o estado de preview (o usuário pausou no controle nativo) + guard-effect limpa `previewing` quando `playbackUrl`/`surface` mudam | B) detectar `seeking`/`onSeeking` para cancelar scrub. **Recomendação:** A — `onPause` cobre pausa manual e o próprio `pause()` do fim; B corre risco de o `seeking` do seek inicial do `startPreview` limpar o estado recém-ligado (ordem de eventos do browser). **Rejeitada:** B nesta fatia (gatilho: relato de preview seguindo quando a pessoa arrasta a barra nativa).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`src/lib/speechExcerptSelection.ts`** (editar o owner puro, sem React/DOM): exportar `excerptPreviewStopAt(range: ExcerptRange, currentSeconds: number): number | null` — devolve `range.endSeconds` quando `currentSeconds` finito e **≥** o fim (fim inclusivo; `onTimeUpdate` pode passar do fim e o retorno é o alvo para clampar de volta), senão `null`. Sem tipo novo (reusa `ExcerptRange`); sem tocar em `MIN_EXCERPT_SECONDS`/`normalizeExcerptRange`/`moveRangeEdge`/C170.
|
||||
- **`src/components/campaign/speech/SpeechDetailPlayer.tsx`** (editar o owner do playback, encaixe cirúrgico):
|
||||
- imports: `PauseIcon` de `lucide-react` junto de `PlayIcon` (:3); `excerptPreviewStopAt` de `@/lib/speechExcerptSelection` (:17-22).
|
||||
- estado: `const [previewing, setPreviewing] = useState(false)` ao lado de `selection` (:137) — `previewing` só é verdadeiro com o `<video>` montado.
|
||||
- `stopPreview()`: `videoRef.current?.pause()` + `setPreviewing(false)` (idempotente).
|
||||
- `applySelection(next: ExcerptRange | null)`: `stopPreview()` e então `setSelection(next)`. Substitui os três call-sites atuais: `onChange={setSelection}` (:443 → `onChange={applySelection}`), `setSelection(initialExcerptRange(...))` (:261) e `setSelection(extendRangeToSegment(...))` (:273). `toggleSelection` (:255-262) usa `applySelection` nos dois ramos. Esse é o dono único do invariante "mudou a seleção → parou".
|
||||
- `startPreview()`: quando há `<video>` montado (`surface === 'vod'` com `playbackUrl`), `video.currentTime = selection.startSeconds; void video.play().catch(() => {}); setPreviewing(true)`. Em `youtubeSurface`, apenas **reposiciona** reusando o ramo YouTube de `seekTo(selection.startSeconds)` (:231-238) — sem `setPreviewing`.
|
||||
- **gesto único no quadrante não resolvido (decisão 3):** `previewIntentRef` + `requestPreview()` — em `vodResolvable` sem `playbackUrl` (e sem resolução em voo), marca `previewIntentRef.current = true` e chama `requestResolution(false)`; um effect em `[resolution, playbackUrl, surface]` inicia o preview quando o arquivo fica pronto (`resolved` + `playbackUrl`) e `previewIntentRef.current`, reusando `startPreview()` depois que o `<video>` monta (seek em `loadedmetadata`, como o effect :169-186); em `generating`/`failed`, limpa `previewIntentRef`. `stopPreview`/`applySelection`/guard-effect também limpam `previewIntentRef`.
|
||||
- `onTimeUpdate` (:245-253, estendido): antes do mapeamento de segmento ativo, se `selection && previewing`, calcular `const stopAt = excerptPreviewStopAt(selection, current)`; quando não-nulo → `video.pause()`, `video.currentTime = stopAt`, `setPreviewing(false)` (fim visível, play pronto); o mapeamento `activeStart` (:249-252) permanece byte a byte.
|
||||
- `<video>` (:291-303): acrescentar `onPause={() => setPreviewing(false)}` (pausa manual/resolução); `controls`, `preload`, `src`, `onTimeUpdate`, `<track>` intocados.
|
||||
- guard-effect: `useEffect(() => { if (surface !== 'vod' || !playbackUrl) setPreviewing(false) }, [surface, playbackUrl])` — nada vaza quando a superfície troca ou o arquivo desmonta.
|
||||
- controle **dentro do card de seleção** (decisão 1): o player renderiza o `Button` e o passa ao card por slot — `<SpeechExcerptControls ... previewState={previewState} previewArea={<Button …/>} />`. `data-slot="speech-excerpt-preview"`, `aria-pressed={previewing}`, mesmas classes do artefato (`min-h-10 w-full sm:w-auto`, `data-icon="inline-start"`):
|
||||
- `surface === 'vod' && playbackUrl` → rótulo **"Pré-visualizar trecho"** (`PlayIcon`); com `previewing`, rótulo **"Parar pré-visualização"** (`PauseIcon`) e `border-primary text-primary` (estado REPRODUZINDO do artefato), clique = `stopPreview()`.
|
||||
- `vodResolvable` ainda não resolvido (quadrante "Assistir o trecho") → rótulo **"Pré-visualizar trecho"** (`PlayIcon`), clique = `requestPreview()`; enquanto a resolução corre, o botão mostra o estado "resolvendo" (ícone/spinner, `disabled`/`aria-busy`) — forma exata a cargo da extensão do artefato (trigger a). A `StatusPanel` "Resolvendo o trecho na Câmara…" já cobre o corpo.
|
||||
- `youtubeSurface` → rótulo **"Pré-visualizar trecho"** (`PlayIcon`), clique = `seekTo(selection.startSeconds)`, **nunca** "Parar" (o app não para o iframe).
|
||||
- Nenhuma das duas superfícies → o player passa a legenda do artefato como `previewArea` ("A seleção permanece ajustável. Pré-visualização não oferecida nesta superfície.") e nenhum botão (cena 4).
|
||||
- o card de seleção ganha `previewState` (`'idle' | 'playing' | 'ended'`) para portar os estados do artefato: `playing` → `ring-1 ring-primary/30` no card + chip "REPRODUZINDO" no cabeçalho; `ended` → chip "FIM VISÍVEL · {fim}"; `idle` → sem chip (composição primária da cena 1). Copy pt-BR de apresentação fica no controle (já tem "Trecho selecionado"/"Início do trecho"); nenhuma lógica de mídia.
|
||||
- aviso honesto do YouTube (literal da intenção): quando `youtubeSurface && selecting`, o **literal** "No YouTube, a pré-visualização começa no início do trecho — parar no fim exige o vídeo da Câmara." é renderizado no rodapé do card de seleção, abaixo do botão (estrutura da cena 3); o caminho da Câmara é o bloco **já existente** do C171 (`renderYoutubeExit`, :391-430, botão "Assistir na Câmara" → `watchVod`) — nenhum botão novo.
|
||||
- linha honesta de "sem superfície" (cena 4): quando `selecting` e não há nem `playbackUrl`/`vodResolvable` nem `youtubeOffsetSeconds`, o `previewArea` é a legenda "A seleção permanece ajustável. Pré-visualização não oferecida nesta superfície." (copy do artefato, sem lógica nova).
|
||||
- intocados: download (:482-506), `SpeechExcerptShare` (:472-481), `SpeechCutDialog` (:521-532), `publishedCut`, `inlineNotice` (:371-386), `renderYoutubeExit`, `aria-busy`, `renderMedia`/`StatusPanel`, transcrição (:546-597), a fileira de ações (:447-515).
|
||||
- **`src/components/campaign/speech/SpeechExcerptControls.tsx`** (dono do card; encaixe cirúrgico, sem lógica de mídia): ganha `previewArea?: ReactNode` (rodapé do card, depois da dica; layout `mt-3 flex flex-wrap items-end justify-between gap-3 border-t border-border pt-3` como a cena 1, com a dica à esquerda) e `previewState?: 'idle' | 'playing' | 'ended'` (default `'idle'`; aplica o `ring` do card e o chip do cabeçalho). A prop `onChange` passa a receber `applySelection` do player. A geometria (track/alças/`role="slider"`/`snap`) fica byte a byte; nada de `videoRef`/`play`/`pause`/`previewing` aqui.
|
||||
- **Migration:** sem migration — nenhum campo/collection/`payload-types` novo (`push: false` intocado).
|
||||
- **Access / Consent:** nenhum `Consent` novo; gate `canReadSpeechCatalog` do detalhe intocado; nenhuma escrita nova; `advisor`/`leader` seguem negados; nada persistido no servidor.
|
||||
- **UI:** Impeccable B — shape → craft → critique → polish; reusa `Button`, `PlayIcon`/`PauseIcon`, `data-icon="inline-start"`, `min-h-10` e o padrão tipográfico `text-xs text-muted-foreground`; portar do artefato os estados **PARADO (sem chip) / REPRODUZINDO (ring + chip + botão primário) / FIM VISÍVEL (chip)** no bloco de seleção, com a barra de seleção intacta; identificadores em inglês, copy pt-BR verbatim do artefato/intenção. A ação **não** migra para a fileira de compartilhar/cortar/baixar (artefato, linha 340).
|
||||
|
||||
### Dados → forma
|
||||
|
||||
**Não se aplica como dado:** o preview é **feedback de ação** (tocar/parar o intervalo), não dado do acervo — mesma decisão do C162/C166. Formas rejeitadas: telemetria/KPI de pré-visualização (a intenção adia), barra de progresso própria do trecho, contador de reproduções, badge de status de **dado do acervo** no card/lista (o chip `REPRODUZINDO`/`FIM VISÍVEL` é estado de controle do artefato, não métrica).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer — função pura + modo de playback (~0,3 dia).** `excerptPreviewStopAt` em `speechExcerptSelection.ts`; `previewing`/`stopPreview`/`applySelection`/`startPreview`/`requestPreview`/`onTimeUpdate`/`onPause`/guard-effect no player; slot `previewArea` no card de seleção. Verificação: `pnpm test:unit -- speechExcerptSelection` e `pnpm test:unit -- speechDetailPlayer`; `pnpm typecheck`.
|
||||
2. **Design + UI + YouTube honesto (~0,4 dia).** **Trigger (a):** o `designer` estende `docs/plans/acervo-player-preview-do-trecho-ui-design.html` com o estado novo (preview no quadrante VOD-a-resolver: botão + estado resolvendo) **antes** de o markup mudar; depois porta classe-a-classe os estados do card/botão (rótulo/realce/ícone/chip), o aviso literal do YouTube no card, a legenda de "sem superfície", mobile ~390 px; casos jsdom restantes. Verificação: `pnpm test:unit`, `pnpm lint`, `pnpm format:check`.
|
||||
3. **Gates (~0,3 dia).** `pnpm gate:fast` (lint + typecheck + unit); e2e curado do acervo **sem entrada nova** no manifesto (`src/components/campaign/speech` já está mapeado) e sem mudança de HTML SSR; `pnpm push`; changelog `docs/changelog/2026-09-16-c173-preview-do-trecho.md` no fechamento.
|
||||
|
||||
## Plano de testes
|
||||
|
||||
- **Unit puro** (`tests/unit/speechExcerptSelection.unit.spec.ts`, estender — casa natural da geometria pura): `excerptPreviewStopAt` — antes do fim → `null`; exatamente no fim → `endSeconds`; depois do fim (overshoot do `timeupdate`) → `endSeconds` (clamp de volta); `currentSeconds` não-finito → `null`; fim inclusivo com `currentSeconds > endSeconds`. Sem DOM, sem fake timers.
|
||||
- **Unit jsdom** (`tests/unit/speechDetailPlayer.unit.spec.tsx`, estender; estender o `beforeAll` de mídia :71-82 com um mock de `pause`/`paused` espelhando o mock de `play`):
|
||||
- **toca no início e para no fim:** `vodResolvable` + resolve o arquivo (`pronto(PLAYBACK_URL, DOWNLOAD_URL)`), `toggleSelection()`, clique em "Pré-visualizar trecho" → `video.currentTime === selection.startSeconds` e `play` chamado; `video.currentTime = endSeconds` (ou além) + `fireEvent.timeUpdate(video)` → `pause` chamado e `video.currentTime === endSeconds` (fim visível) e o rótulo volta a "Pré-visualizar trecho".
|
||||
- **para quando a seleção muda:** durante o preview, clique em outra frase (`button[data-start-seconds]`) ou arraste uma alça → `pause` chamado, `previewing` desligado (rótulo de preview volta) e a nova range refletida nos `aria-valuenow` (nunca continua tocando o intervalo antigo).
|
||||
- **gesto único (VOD a resolver):** `vodResolvable` sem `playbackUrl`, `toggleSelection()`, clique em "Pré-visualizar trecho" → resolução disparada; quando chega `pronto(PLAYBACK_URL, …)`, o `<video>` monta e `play` é chamado com `currentTime` no início do trecho; em `gerando`/falha, nenhum play e a intenção é limpa.
|
||||
- **YouTube honesto:** `youtubeVideoId` + offset, `toggleSelection()`, clique em "Pré-visualizar trecho" → `iframe.src` contém `start = offset + selection.start` (reuso de `seekTo`), **sem** `<video>` e rótulo **nunca** "Parar pré-visualização"; o aviso literal "No YouTube, a pré-visualização começa no início do trecho — parar no fim exige o vídeo da Câmara." presente; com `vodResolvable`, "Assistir na Câmara" presente (C171), sem novo botão.
|
||||
- **não é oferecida sem superfície parável:** sem YouTube e sem `vodResolvable`/`playbackUrl` → legenda de "não oferecida", seleção ainda compartilhável/cortável quando aplicável.
|
||||
- os blocos C162/C166/C167/C171/`no-video` existentes continuam verdes (nenhum pino existente muda).
|
||||
- **E2E: não agrega valor aqui — declarado.** `tests/e2e/campaignSpeechAcervo.e2e.spec.ts` é asserção de HTML server-rendered e o preview só existe com `selection` (estado client, ausente no SSR); não há interação de playback no harness browserless (que ainda aborta `youtube*`). Nenhuma entrada nova no manifesto curado; nenhum pino de e2e muda.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- IFrame API/`enablejsapi`/`postMessage`/`end` do embed para "parar" no YouTube — a postura honesta cobre (intenção, corte explícito).
|
||||
- Segundo player/superfície, iframe próprio, render/export/trim, arquivo novo de preview — anti-goals da intenção.
|
||||
- Loop contínuo, A/B do trecho, waveform, controle de velocidade, scrub próprio, "pular para o fim" — fora.
|
||||
- Cancelar preview no `seeking`/scrub nativo (decisão 7, risco de race) — gatilho registrado.
|
||||
- Reescrever a **geometria** de `SpeechExcerptControls` (track/alças/ímã/`snap`/`role="slider"`) ou colocar lógica de mídia nele — o card só ganha o slot `previewArea` + `previewState`; o mínimo 5 s, o compartilhamento/corte (C166/C167/C168) e a biblioteca ficam fora.
|
||||
- `Consent`/collection/migration/escrita/telemetria; tocar `resolver-vod`/`actions/speech.ts` (C169) — fora.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Serializa com C172** (mesmo `SpeechDetailPlayer.tsx`): encaixar depois (ou antes, combinado) para não misturar diffs; o toque é cirúrgico no arquivo, sem reescrita de `renderMedia`/quadrantes.
|
||||
- **Overshoot do `timeupdate` (~4 Hz):** o fim pode ser ultrapassado em ~250 ms — `excerptPreviewStopAt` devolve `endSeconds` e o `onTimeUpdate` faz `video.currentTime = stopAt` antes de pausar; teste puro cobre o clamp.
|
||||
- **Estado `previewing` preso (pausa nativa/troca de superfície):** `onPause` fecha o estado e o guard-effect em `[surface, playbackUrl]` limpa quando o `<video>` desmonta; teste de seleção mudando cobre o caminho principal.
|
||||
- **Vazamento do trecho antigo:** um único `applySelection` em todos os call-sites de `setSelection` (:261/:273/:443) — nenhum caminho escreve `selection` sem parar o preview.
|
||||
- **Falsa promessa no YouTube:** o rótulo "Parar pré-visualização" e a pausa no fim são gated a `surface === 'vod'` com `<video>` montado; no YouTube só reposiciona e o aviso literal aparece; teste jsdom pinna o rótulo.
|
||||
- **Gesto único (resolve→play) com corrida de estado:** a intenção pendente pode sobreviver a uma troca de seleção/superfície — `applySelection`/`stopPreview`/guard-effect limpam `previewIntentRef`; `generating`/`failed` também; teste cobre o caminho e a limpeza no erro.
|
||||
- **Porte visual do card (chip/ring/rodapé):** o artefato desenha o botão e o chip de estado dentro do card; o porte é classe-a-classe (decisão 1) e a **crítica final do `designer` (trigger c)** confere a paridade contra o app renderizado (390/1280 + estados PARADO/REPRODUZINDO/FIM VISÍVEL/RESOLVENDO); divergência de detalhe é ajuste no card, sem mover lógica.
|
||||
- **Regressão C162/C166/C167/C170/C171:** ramos de superfície, transcrição, download, share, cut e clamps intocados; as unidades existentes (745 linhas) rodam a cada fase; `excerptPreviewStopAt` novo é consumido pelo componente (knip sem export morto) e `PauseIcon` é usado.
|
||||
- **C169 concorrente no domínio:** nenhuma dependência de código; só torna o caminho "Assistir na Câmara" mais confiável.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto: com trecho selecionado no vídeo da Câmara, "Pré-visualizar trecho" posiciona no início, toca e **para no fim**; mudar a seleção cancela e reflete o novo intervalo; no YouTube a postura é honesta (reposiciona + aviso + Câmara via C171), sem parada prometida; seleção/share/corte intactos; transcrição C166 intacta.
|
||||
- [ ] Invariantes AGENTS/engineering-standards: sem migration/`Consent`/escrita/segundo player; `lib/` segue puro e client-safe; um dono do playback (`SpeechDetailPlayer`); copy pt-BR / identificadores em inglês; nenhum comentário supérfluo; nenhum twin (`excerptPreviewStopAt` é a única função nova e é a dona da regra).
|
||||
- [ ] Testes de domínio previstos onde o contrato do player muda: unit puro (`speechExcerptSelection`) + unit jsdom (`speechDetailPlayer`) verdes; e2e declarado sem valor (SSR-only) e sem entrada nova no manifesto; `pnpm gate:fast` limpo.
|
||||
|
||||
## Self-score decision-quality
|
||||
|
||||
5/5 — (1) as decisões caras (posição do controle, postura no YouTube, gating por superfície, fim/manual stop, dono da matemática, cancelamento por seleção) têm opções + recomendação + rejeitadas explícitas; (2) cabe no appetite herdado porque corta IFrame API, resolução encadeada, segundo player e arquivo novo — uma função pura + o dono existente + o card de seleção com um slot + as duas suítes unit; (3) rabbit holes nomeados com corte (IFrame API, auto-resolve, loop, scrub nativo, telemetria); (4) depth check: reusa `SpeechDetailPlayer`, o card de seleção existente, `Button`/`PlayIcon`/`PauseIcon`, o `seekTo` do YouTube, o bloco de saída C171 e `speechExcerptSelection` — nenhum shell/twin/componente paralelo; (5) o outcome da intenção permanece intacto — a engenharia não reescreveu o aceite; a posição do botão e a postura no YouTube seguem o artefato de gate (nenhum ponto de produto em aberto).
|
||||
|
|
@ -70,6 +70,12 @@
|
|||
.btn-secondary {
|
||||
@apply bg-secondary text-secondary-foreground;
|
||||
}
|
||||
.btn-resolving {
|
||||
@apply cursor-wait border-border bg-muted/50 text-muted-foreground opacity-60;
|
||||
}
|
||||
.spinner {
|
||||
@apply size-4 shrink-0 animate-spin fill-none stroke-current stroke-2;
|
||||
}
|
||||
.icon {
|
||||
@apply size-4 shrink-0 fill-none stroke-current stroke-2;
|
||||
}
|
||||
|
|
@ -86,6 +92,9 @@
|
|||
.selection-card {
|
||||
@apply rounded-lg border border-border bg-muted/30 p-3;
|
||||
}
|
||||
.status-panel {
|
||||
@apply flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-lg border border-border bg-muted/40 px-6 text-center text-muted-foreground;
|
||||
}
|
||||
.video-placeholder {
|
||||
background:
|
||||
radial-gradient(circle at 50% 34%, rgb(255 255 255 / 9%), transparent 24%),
|
||||
|
|
@ -104,6 +113,16 @@
|
|||
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>
|
||||
<div class="mt-4 rounded-lg border border-primary/20 bg-primary/5 px-4 py-3">
|
||||
<p class="text-xs font-semibold tracking-wide text-primary uppercase">
|
||||
Extensão trigger (a) · resolve → toca em um gesto
|
||||
</p>
|
||||
<p class="mt-1 text-xs leading-5 text-foreground/80">
|
||||
Acrescenta o quadrante em que o vídeo da Câmara ainda não foi resolvido. O pedido de
|
||||
pré-visualização fica pendente somente durante a resolução e toca o intervalo assim que
|
||||
o arquivo verificado chega.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- CENA 1: DESKTOP -->
|
||||
|
|
@ -641,6 +660,420 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CENA 5: EXTENSÃO TRIGGER (A), VOD A RESOLVER -->
|
||||
<section class="mt-10" aria-labelledby="resolve-preview-title">
|
||||
<div class="scene-label" id="resolve-preview-title">
|
||||
Cena 05 · Trigger (a) · Câmara a resolver · gesto único resolve → toca
|
||||
</div>
|
||||
|
||||
<div class="frame mx-auto w-full max-w-[1280px] p-5 sm:p-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Desktop · viewport de referência 1280 px</p>
|
||||
<p class="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
O mesmo quadrante em três momentos. Painel e card coexistem; a ação continua dentro
|
||||
do card de seleção.
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex h-6 items-center rounded-full bg-primary/10 px-2.5 text-[10px] font-semibold tracking-wide text-primary uppercase"
|
||||
>Estado acrescentado ao gate</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-4 lg:grid-cols-3">
|
||||
<!-- 5A: unresolved, ready for the single gesture -->
|
||||
<article aria-labelledby="resolve-idle-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold text-foreground" id="resolve-idle-title">
|
||||
1 · Ainda não resolvido
|
||||
</h3>
|
||||
<span class="text-[10px] font-medium text-muted-foreground">PRONTO PARA PEDIR</span>
|
||||
</div>
|
||||
<div class="status-panel">
|
||||
<svg class="icon size-8" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m10 9 5 3-5 3z" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-foreground/90">
|
||||
O trecho deste vídeo é gerado pela Câmara dos Deputados.
|
||||
</p>
|
||||
<p class="max-w-md text-xs leading-5">
|
||||
Clique para resolver o arquivo exato desta fala — ele é verificado antes de tocar.
|
||||
</p>
|
||||
<button class="btn btn-primary mt-1" 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 o trecho
|
||||
</button>
|
||||
</div>
|
||||
<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-3 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="border-t border-border pt-3">
|
||||
<button class="btn btn-outline 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>
|
||||
</article>
|
||||
|
||||
<!-- 5B: resolving with pending preview intent -->
|
||||
<article aria-labelledby="resolve-busy-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold text-foreground" id="resolve-busy-title">
|
||||
2 · Resolvendo
|
||||
</h3>
|
||||
<span
|
||||
class="inline-flex h-5 items-center gap-1.5 rounded-full bg-secondary px-2 text-[10px] font-medium text-muted-foreground"
|
||||
><svg class="spinner size-3" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.6" /></svg
|
||||
>AGUARDANDO ARQUIVO</span
|
||||
>
|
||||
</div>
|
||||
<div class="status-panel" role="status" aria-live="polite">
|
||||
<svg
|
||||
class="spinner size-6"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Resolvendo o trecho na Câmara"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.6" />
|
||||
</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="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-3 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="border-t border-border pt-3">
|
||||
<button
|
||||
class="btn btn-outline btn-resolving w-full"
|
||||
type="button"
|
||||
aria-busy="true"
|
||||
disabled
|
||||
>
|
||||
<svg class="spinner" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.6" />
|
||||
</svg>
|
||||
Resolvendo trecho…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- 5C: resolution failed, preview intent discarded -->
|
||||
<article aria-labelledby="resolve-failed-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold text-foreground" id="resolve-failed-title">
|
||||
3 · Falhou
|
||||
</h3>
|
||||
<span class="text-[10px] font-medium text-muted-foreground"
|
||||
>INTENÇÃO DESCARTADA</span
|
||||
>
|
||||
</div>
|
||||
<div class="status-panel" role="status" aria-live="polite">
|
||||
<svg class="icon size-8" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m4 20 16-16" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-foreground/90">
|
||||
Não foi possível carregar o vídeo deste trecho.
|
||||
</p>
|
||||
<p class="max-w-md text-xs leading-5">
|
||||
A Câmara não entregou o arquivo agora. A transcrição e a fonte oficial continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
<button class="btn btn-outline mt-1" type="button">Tentar novamente</button>
|
||||
</div>
|
||||
<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-3 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="border-t border-border pt-3">
|
||||
<button class="btn btn-outline 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>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 rounded-lg border border-border bg-muted/30 px-4 py-3">
|
||||
<p class="text-xs font-medium text-foreground/90">Regra visual desta extensão</p>
|
||||
<p class="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
O botão ocupado não usa Mandate Red, não muda de lugar e não vira “Parar
|
||||
pré-visualização”. Ele fica outline, desabilitado, com spinner e o texto “Resolvendo
|
||||
trecho…”. “Parar pré-visualização” só aparece depois que o arquivo chegou e o vídeo
|
||||
está efetivamente reproduzindo. Compartilhar, cortar e baixar continuam na fileira
|
||||
externa, sem receber esta ação.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5D: same state ladder at the mobile viewport -->
|
||||
<div class="mt-6">
|
||||
<p class="mb-3 text-xs font-medium text-muted-foreground">
|
||||
Mobile · viewport de referência 390 px · mesmos três momentos
|
||||
</p>
|
||||
<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="space-y-6 p-4">
|
||||
<article aria-labelledby="mobile-resolve-idle-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold" id="mobile-resolve-idle-title">
|
||||
Ainda não resolvido
|
||||
</h3>
|
||||
<span class="text-[10px] text-muted-foreground">1 / 3</span>
|
||||
</div>
|
||||
<div class="status-panel min-h-[202px] aspect-auto py-5">
|
||||
<svg class="icon size-7" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m10 9 5 3-5 3z" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-foreground/90">
|
||||
O trecho deste vídeo é gerado pela Câmara dos Deputados.
|
||||
</p>
|
||||
<p class="text-xs leading-5">
|
||||
Clique para resolver o arquivo exato desta fala — ele é verificado antes de
|
||||
tocar.
|
||||
</p>
|
||||
<button class="btn btn-primary mt-1 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>
|
||||
Assistir o trecho
|
||||
</button>
|
||||
</div>
|
||||
<div class="selection-card mt-3">
|
||||
<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-2 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="mt-3 border-t border-border pt-3">
|
||||
<button class="btn btn-outline 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>
|
||||
</article>
|
||||
|
||||
<div class="h-px bg-border"></div>
|
||||
|
||||
<article aria-labelledby="mobile-resolve-busy-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold" id="mobile-resolve-busy-title">Resolvendo</h3>
|
||||
<span class="text-[10px] text-muted-foreground">2 / 3</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-panel min-h-[202px] aspect-auto py-5"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<svg
|
||||
class="spinner size-6"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Resolvendo o trecho na Câmara"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.6" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-foreground/90">
|
||||
Resolvendo o trecho na Câmara…
|
||||
</p>
|
||||
<p class="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="selection-card mt-3">
|
||||
<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-2 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="mt-3 border-t border-border pt-3">
|
||||
<button
|
||||
class="btn btn-outline btn-resolving h-11 w-full"
|
||||
type="button"
|
||||
aria-busy="true"
|
||||
disabled
|
||||
>
|
||||
<svg class="spinner" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.6" />
|
||||
</svg>
|
||||
Resolvendo trecho…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="h-px bg-border"></div>
|
||||
|
||||
<article aria-labelledby="mobile-resolve-failed-title">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-semibold" id="mobile-resolve-failed-title">Falhou</h3>
|
||||
<span class="text-[10px] text-muted-foreground">3 / 3</span>
|
||||
</div>
|
||||
<div
|
||||
class="status-panel min-h-[222px] aspect-auto py-5"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<svg class="icon size-7" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<path d="m4 20 16-16" />
|
||||
</svg>
|
||||
<p class="text-sm font-medium text-foreground/90">
|
||||
Não foi possível carregar o vídeo deste trecho.
|
||||
</p>
|
||||
<p class="text-xs leading-5">
|
||||
A Câmara não entregou o arquivo agora. A transcrição e a fonte oficial continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
<button class="btn btn-outline mt-1 h-11 w-full" type="button">
|
||||
Tentar novamente
|
||||
</button>
|
||||
</div>
|
||||
<div class="selection-card mt-3">
|
||||
<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-2 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="mt-3 border-t border-border pt-3">
|
||||
<button class="btn btn-outline 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>
|
||||
<p class="mt-3 text-[11px] leading-5 text-muted-foreground">
|
||||
A falha descarta a intenção anterior. O controle volta ao estado normal; nada toca
|
||||
sozinho depois de um “Tentar novamente” separado.
|
||||
</p>
|
||||
</article>
|
||||
</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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue