mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
A lista /campanha/comunicacao/conteudos ganha um modo de seleção opt-in que envolve os dois corpos (tabela e card list) com um estado client compartilhado, chaveado pela URL canônica (a seleção morre em página/filtro e nunca cresce para além da página visível). Com seleção ativa, a barra flutuante aplica o mesmo veredito unitário à seleção: Publicar/Despublicar agem direto com recibo honesto; Apagar sempre confirma no CampaignDeleteDialog reusado (novo caminho onConfirm aditivo), avisando os links públicos só quando a seleção carrega publicadas. O transporte é uma rota única (POST .../conteudos/lote, campaignJsonMutationRoute, teto de 50 ids) que itera server-side as ações donas: cada peça mantém a própria transação, o gate fresco e a limpeza best-effort de mídia; a falha de uma nunca reverte as demais e o recibo separa afetadas x falhas (2 de 3 peças apagadas. 1 falhou.). Sem migration, sem Consent novo, sem rota pública (S27) e sem mudar a semântica do C222. Cobertura: int do lote (papéis, dedupe, mídia, evento, tag, falha parcial, schema), unit do builder/barra/seleção e e2e HTTP. Design tier: DEGRADED (artefato de origem sem certificação frontier) — crítica do designer aplicada no escopo do item; entrega bloqueada para sign-off humano antes do PR. Related #1388
156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
'use client'
|
||
|
||
import { SquareCheckIcon, XIcon } from 'lucide-react'
|
||
import type { ReactNode } from 'react'
|
||
|
||
import { useContentPieceSelection } from '@/components/campaign/content/ContentPieceSelectionProvider'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Checkbox } from '@/components/ui/Checkbox'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
/**
|
||
* C236 — the affordances of the selection mode (approved design scenes 1–3):
|
||
* the single entry at the top of the list, the mode badge with its exit, the
|
||
* per-row checkboxes of the table and the mobile card frame. All of them are
|
||
* client islands reading the one selection context; the table shell and the
|
||
* card bodies stay server-rendered.
|
||
*/
|
||
|
||
/** The opt-in entry ("Selecionar") and the active-mode chrome of the top of the list. */
|
||
export const ContentPieceSelectionModeControl = () => {
|
||
const { selectionMode, enterSelection, exitSelection, pendingAction } = useContentPieceSelection()
|
||
|
||
if (!selectionMode) {
|
||
return (
|
||
<div className="mt-4 flex items-center justify-end">
|
||
<Button type="button" variant="outline" className="min-h-11" onClick={enterSelection}>
|
||
<SquareCheckIcon data-icon="inline-start" aria-hidden="true" />
|
||
Selecionar
|
||
</Button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="mt-4 flex items-center justify-between gap-3">
|
||
<span className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
||
<SquareCheckIcon className="size-3.5" aria-hidden="true" />
|
||
Modo de seleção
|
||
</span>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
className="min-h-11 text-muted-foreground hover:text-foreground"
|
||
aria-label="Cancelar seleção"
|
||
disabled={pendingAction !== null}
|
||
onClick={exitSelection}
|
||
>
|
||
<XIcon data-icon="inline-start" aria-hidden="true" />
|
||
<span aria-hidden="true" className="max-sm:hidden">
|
||
Cancelar seleção
|
||
</span>
|
||
<span aria-hidden="true" className="sm:hidden">
|
||
Cancelar
|
||
</span>
|
||
</Button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** The header checkbox: all of the visible page, or none. */
|
||
export const ContentPieceSelectAllCheckbox = () => {
|
||
const { allSelected, selectedCount, toggleAll, pendingAction } = useContentPieceSelection()
|
||
|
||
return (
|
||
<span className="grid size-9 place-items-center">
|
||
<Checkbox
|
||
checked={allSelected ? true : selectedCount > 0 ? 'indeterminate' : false}
|
||
onCheckedChange={() => toggleAll()}
|
||
disabled={pendingAction !== null}
|
||
aria-label="Selecionar todas as peças desta página"
|
||
/>
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* One table-row checkbox. The wrapper carries `data-selection-state`, which the
|
||
* row's `has-[…]:bg-primary/5` class uses to tint the selected line.
|
||
*/
|
||
export const ContentPieceRowCheckbox = ({
|
||
contentPieceId,
|
||
title,
|
||
}: {
|
||
contentPieceId: number
|
||
title: string
|
||
}) => {
|
||
const { isSelected, toggle, pendingAction } = useContentPieceSelection()
|
||
const selected = isSelected(contentPieceId)
|
||
|
||
return (
|
||
<span
|
||
className="grid size-9 place-items-center"
|
||
data-selection-state={selected ? 'selected' : undefined}
|
||
>
|
||
<Checkbox
|
||
checked={selected}
|
||
onCheckedChange={() => toggle(contentPieceId)}
|
||
disabled={pendingAction !== null}
|
||
aria-label={`Selecionar ${title}`}
|
||
/>
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* The mobile card frame (design scene 3b): in selection mode the article gains
|
||
* the 44px touch target with the checkbox and the primary ring; outside it the
|
||
* frame renders the exact resting article, so the card look at rest does not
|
||
* depend on this island. `header` is the card's title/meta block (which sits
|
||
* beside the checkbox); children are the rest of the card body.
|
||
*/
|
||
export const ContentPieceCardSelectionFrame = ({
|
||
contentPieceId,
|
||
title,
|
||
header,
|
||
children,
|
||
}: {
|
||
contentPieceId: number
|
||
title: string
|
||
header: ReactNode
|
||
children: ReactNode
|
||
}) => {
|
||
const { selectionMode, isSelected, toggle, pendingAction } = useContentPieceSelection()
|
||
const selected = selectionMode && isSelected(contentPieceId)
|
||
|
||
if (!selectionMode) {
|
||
return (
|
||
<article className="rounded-xl border bg-card p-4">
|
||
{header}
|
||
{children}
|
||
</article>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<article
|
||
className={cn(
|
||
'rounded-xl border border-border bg-card p-4',
|
||
selected && 'ring-1 ring-primary/25',
|
||
)}
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
<span className="grid size-11 shrink-0 place-items-center">
|
||
<Checkbox
|
||
checked={selected}
|
||
onCheckedChange={() => toggle(contentPieceId)}
|
||
disabled={pendingAction !== null}
|
||
aria-label={`Selecionar ${title}`}
|
||
/>
|
||
</span>
|
||
<div className="min-w-0 flex-1">{header}</div>
|
||
</div>
|
||
{children}
|
||
</article>
|
||
)
|
||
}
|