← Biblioteca componentes/conversao/selo-prazo/ficha.md CódigoTela cheia
conversao/selo-prazo

Selo de prazo e vagas

🟢 estavel complexidade média prefixo prz- escassez prazo vagas evento inscricao contagem cta

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/conversao/selo-prazo/estilo.css">
<script src="componentes/conversao/selo-prazo/script.js"></script>

markup.html · 51 linhas · 2,8 KB

<!-- ==========================================================================
     SELO DE PRAZO E VAGAS — a escassez que só aparece se for verdade
     Prefixo: prz-
     Requer: core (tokens, base, motion.css, motion.js) + este estilo.css e
             este script.js

     Um bloco de chamada com o selo de quanto falta: "Faltam 3 dias",
     "Restam 2 de 12 vagas", e uma barra de lugares ocupados. Serve a evento,
     turma, lista de espera, agenda do mês de um sócio.

     TUDO É CONCEDIDO, NADA É ESCRITO. O bloco declara o prazo
     (data-prz-ate, com fuso) e as vagas (data-prz-vagas, data-prz-total) e
     QUANDO elas foram contadas (data-prz-contado). O script escreve o selo:
       - prazo futuro      "Faltam 3 dias" / "Faltam 5 horas"
       - prazo vencido     o botão SAI DO DOM e entra a mensagem de encerrado
       - vagas contadas    "Restam 2 de 12 vagas" e a barra
       - contagem velha    (mais de data-prz-validade dias, 2 por padrão)
                           o selo de vagas NÃO aparece — número de vagas de
                           semana passada é pressão falsa
       - vagas zeradas     o botão sai e entra a mensagem de esgotado
     Sem JavaScript, nenhum selo: a peça não afirma escassez sem conferir.
     Mesmo método da listas/agenda-eventos e do dados/cards-cotacoes.
     ========================================================================== -->

<section class="sec-deep sec-pad">
  <div class="wrap">
    <div class="prz" data-prz
         data-prz-ate="2026-10-12T23:59:00-03:00"
         data-prz-vagas="2" data-prz-total="12" data-prz-contado="2026-09-29"
         data-rv>

      <div class="prz-texto">
        <span class="eyebrow">Encontro presencial · 14 out · São Paulo</span>
        <h2 class="sec-title ttl">Café com os sócios: <em>sucessão em 2027</em></h2>
        <p class="prz-apoio">Uma hora e meia sobre o que muda no calendário da reforma. Doze lugares, gratuito para clientes e convidados.</p>
      </div>

      <div class="prz-lado">
        <p class="prz-selos" data-prz-selos></p>
        <div class="prz-barra" data-prz-barra aria-hidden="true" hidden><span class="prz-barra-in"></span></div>
        <a class="btn btn-primary prz-acao" href="#reservar" data-prz-acao>
          Reservar meu lugar
          <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
        </a>
        <p class="prz-fim" data-prz-encerrado hidden>As inscrições para este encontro foram encerradas.</p>
        <p class="prz-fim" data-prz-esgotado hidden>Os lugares deste encontro se esgotaram. Avisamos você do próximo.</p>
      </div>

    </div>
  </div>
</section>

estilo.css · 57 linhas · 2,4 KB

/* ==========================================================================
   SELO DE PRAZO E VAGAS — prz-
   Um bloco de chamada com o selo de quanto falta, concedido pelo script.
   Tokens usados: --bg --fg --fg-2 --fg-muted --rule --rule-2 --accent
                  --radius-media --radius-pill --ease
   ========================================================================== */

.prz{
  display:grid;grid-template-columns:minmax(0,7fr) minmax(0,4fr);
  gap:clamp(24px,4vw,64px);align-items:end;
  padding:clamp(26px,3.4vw,48px);
  border:1px solid var(--rule);border-radius:var(--radius-media);
  background:color-mix(in srgb,var(--fg) 4%,var(--bg));
}
.prz-apoio{margin-top:16px;font-size:15.5px;line-height:1.6;color:var(--fg-2);max-inline-size:56ch}

.prz-lado{display:grid;gap:14px;justify-items:start}

/* Os selos nascem vazios: sem script, nenhum. O script escreve um <span> por
   afirmação que consegue sustentar (prazo, vagas). */
.prz-selos{display:flex;flex-wrap:wrap;gap:8px}
.prz-selos:empty{display:none}
.prz-selo{
  display:inline-flex;align-items:center;gap:8px;
  font-size:12.5px;letter-spacing:.02em;color:var(--fg);
  border:1px solid var(--rule-2);border-radius:var(--radius-pill);padding:6px 12px;
}
.prz-selo::before{content:"";inline-size:7px;block-size:7px;border-radius:50%;background:var(--fg-muted)}
/* Perto do fim (menos de 3 dias, ou até 20% das vagas): o ponto acende na cor
   de marca. É a única ênfase — nada pisca, nada treme. */
.prz-selo.is-perto{border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
.prz-selo.is-perto::before{background:var(--accent)}

/* A barra de lugares ocupados. A largura entra por --prz-k (0 a 1), escrita
   pelo script. aria-hidden: o selo já diz o número por extenso. */
.prz-barra{
  inline-size:100%;block-size:4px;border-radius:var(--radius-pill);
  background:var(--rule);overflow:hidden;
}
.prz-barra[hidden]{display:none}
.prz-barra-in{
  display:block;block-size:100%;inline-size:100%;
  background:var(--accent);
  transform-origin:0 50%;transform:scaleX(var(--prz-k,0));
  transition:transform .9s var(--ease);
}
@media(prefers-reduced-motion:reduce){
  .prz-barra-in{transition:none}
}

.prz-acao{margin-top:4px}
.prz-fim{font-size:14px;line-height:1.55;color:var(--fg-2);max-inline-size:36ch}
.prz-fim[hidden]{display:none}

@media(max-width:860px){
  .prz{grid-template-columns:1fr;align-items:start}
}

script.js · 103 linhas · 4,3 KB

/* ==========================================================================
   SELO DE PRAZO E VAGAS — prz-
   Concede os selos a partir do que o bloco declara, e só do que ele
   consegue sustentar:

     data-prz-ate          prazo, ISO com fuso. Vencido: o botão [data-prz-acao]
                           sai do DOM e aparece [data-prz-encerrado].
     data-prz-vagas        vagas restantes, e data-prz-total, o total.
     data-prz-contado      AAAA-MM-DD em que as vagas foram contadas.
     data-prz-validade     dias em que essa contagem vale (2). Passou disso,
                           o selo de vagas não aparece: número velho de vagas
                           é pressão falsa. Vagas zeradas: o botão sai e
                           aparece [data-prz-esgotado].

   Por que o botão sai do DOM e não só da vista: escondido por CSS, ele
   continuaria no Tab e no leitor de tela, oferecendo reserva num evento
   encerrado. Mesmo motivo da listas/agenda-eventos.

   A barra anima por transition de CSS (o script só escreve --prz-k), e o
   @media(prefers-reduced-motion) do estilo.css a desliga — por isso nada
   aqui consulta LIB.reduce: não há movimento em JS para desligar.
   Reavalia a cada minuto.
   ========================================================================== */
(function () {
  var raizes = document.querySelectorAll('[data-prz]');
  if (!raizes.length) return;                  // guard: sem o componente, saída silenciosa

  var DIA = 864e5, HORA = 36e5;

  function selo(texto, perto) {
    var s = document.createElement('span');
    s.className = 'prz-selo' + (perto ? ' is-perto' : '');
    s.textContent = texto;
    return s;
  }

  /* Tira o botão de cena de vez e mostra a mensagem que o substitui. */
  function encerrar(raiz, qual) {
    var acao = raiz.querySelector('[data-prz-acao]');
    if (acao && acao.parentNode) acao.parentNode.removeChild(acao);
    var msg = raiz.querySelector(qual);
    if (msg) msg.hidden = false;
    var barra = raiz.querySelector('[data-prz-barra]');
    if (barra) barra.hidden = true;
  }

  function avaliar(raiz) {
    var caixa = raiz.querySelector('[data-prz-selos]');
    var barra = raiz.querySelector('[data-prz-barra]');
    if (!caixa) return;
    caixa.textContent = '';
    var agora = Date.now();

    /* ---- prazo ---- */
    var ate = Date.parse(raiz.getAttribute('data-prz-ate') || '');
    if (!isNaN(ate)) {
      var falta = ate - agora;
      if (falta <= 0) {
        raiz.setAttribute('data-estado', 'encerrado');
        encerrar(raiz, '[data-prz-encerrado]');
        return;
      }
      var dias = Math.floor(falta / DIA);
      var texto = dias >= 1
        ? (dias === 1 ? 'Falta 1 dia' : 'Faltam ' + dias + ' dias')
        : (Math.ceil(falta / HORA) <= 1 ? 'Falta menos de 1 hora' : 'Faltam ' + Math.ceil(falta / HORA) + ' horas');
      caixa.appendChild(selo(texto, dias < 3));
    }

    /* ---- vagas: só com contagem recente ---- */
    var vagas = parseInt(raiz.getAttribute('data-prz-vagas'), 10);
    var total = parseInt(raiz.getAttribute('data-prz-total'), 10);
    var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raiz.getAttribute('data-prz-contado') || '');
    var validade = parseFloat(raiz.getAttribute('data-prz-validade'));
    if (!(validade > 0)) validade = 2;
    if (!isNaN(vagas) && m) {
      var contado = new Date(+m[1], +m[2] - 1, +m[3], 12).getTime();
      var idade = (agora - contado) / DIA;
      if (idade >= -1 && idade <= validade) {
        if (vagas <= 0) {
          raiz.setAttribute('data-estado', 'esgotado');
          caixa.textContent = '';
          encerrar(raiz, '[data-prz-esgotado]');
          return;
        }
        var temTotal = total > 0 && vagas <= total;
        caixa.appendChild(selo(
          (vagas === 1 ? 'Resta 1' : 'Restam ' + vagas) + (temTotal ? ' de ' + total : '') + (vagas === 1 ? ' vaga' : ' vagas'),
          temTotal ? vagas / total <= .2 : vagas <= 2));
        if (barra && temTotal) {
          barra.hidden = false;
          barra.style.setProperty('--prz-k', ((total - vagas) / total).toFixed(3));
        }
      }
    }
    raiz.setAttribute('data-estado', 'aberto');
  }

  [].forEach.call(raizes, function (raiz) {
    avaliar(raiz);
    setInterval(function () { if (raiz.getAttribute('data-estado') === 'aberto') avaliar(raiz); }, 60000);
  });
})();

A escassez que só aparece se for verdade. Um bloco de chamada — olho, título, apoio, botão — com o selo de quanto falta ao lado: "Faltam 3 dias", "Restam 2 de 12 vagas", e uma barra de lugares ocupados. Nada disso está escrito no markup: o bloco declara o prazo e a contagem, e o script concede só o que consegue sustentar.

O problema que ela resolve: "últimas vagas" é o truque mais velho de página de venda, e por isso mesmo o leitor desconfia dele. Aqui o selo carrega o próprio lastro: tem prazo com fuso, tem contagem com data, e quando qualquer um dos dois deixa de valer, o selo some — e, se o prazo venceu, o botão sai junto.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho o que, quando e onde
Título o assunto, 3 a 9 palavras
Apoio duração, lotação, custo
Botão reservar, inscrever, garantir
Encerrado o que acontece agora e, se houver, o próximo caminho
Esgotado com a promessa de avisar do próximo — só se ela for cumprida

O que mantém a peça: o selo é consequência, não argumento. O título e o apoio têm de convencer sozinhos; o selo só diz quanto tempo a pessoa tem para decidir.

Especificação dos assets

Nenhum.


Como usar

<link rel="stylesheet" href="componentes/conversao/selo-prazo/estilo.css">
<script src="componentes/conversao/selo-prazo/script.js"></script>

O bloco declara:

<div class="prz" data-prz
     data-prz-ate="2026-10-12T23:59:00-03:00"
     data-prz-vagas="2" data-prz-total="12" data-prz-contado="2026-09-29">

Ganchos

Gancho Papel
data-prz-ate prazo, ISO 8601 com fuso
data-prz-vagas / data-prz-total vagas restantes e total
data-prz-contado dia em que as vagas foram contadas, AAAA-MM-DD
data-prz-validade dias em que a contagem vale. 2
[data-prz-acao] o botão que sai do DOM quando acaba
[data-prz-encerrado] / [data-prz-esgotado] as mensagens que o substituem; nascem [hidden]
.prz[data-estado] aberto, encerrado ou esgotado, escrito pelo script

Variações


Compliance


Gotchas


Movimento reduzido / toque

A barra preenche por transição de 0,9s; com reduce, aparece no lugar. Nada depende de hover.


Histórico

Origem Quando O quê
referência: cta.gallery 2026-09-29 Clone de referência, reescrito do zero

Referências visuais: o selo "2 Spots Remaining!" (Harry Solaris) e a régua de horários ocupados (bakedwith), via cta.gallery. Entrou por decisão do dono, com "dias restantes" como caso principal; o método é o da listas/agenda-eventos e do dados/cards-cotacoes. Registro consolidado em projetos/referencias-externas.md.