Pular para o conteúdo
Biblioteca AUT-01

Login em tela dividida

Formulário à esquerda e mídia à direita com véu em degradê; os campos entram escalonados. No celular a mídia sai e sobra o formulário.

usa o AUT-01
abrir sozinho ↗
Código
AUT-01 · alias login-dividido
Função
Autenticação
Fonte
21st.dev · Auth Form Split Screen
Recebido
25/09/2026
Status
adotado
Licença
não verificada

O código

login-dividido.css + login-dividido.js + markup.html 137 linhas · 3 arquivo(s) numa caixa só
/* ===== login-dividido.css ========================================================== */

/* AUT-01 · Login em tela dividida — reescrito do original do 21st.dev sem React, sem zod e sem
   react-hook-form. ENTREGA O INVÓLUCRO E O PAINEL DE MÍDIA, não o formulário: o formulário é o do
   hospedeiro (no caso que originou isto, o do Odoo). Os campos do exemplo não validam nada.

   Abaixo de 1024 px o painel de mídia não aparece e sobra o formulário, centrado.
   Os filhos do formulário entram escalonados, como no original. */
.c-login {
  --c-aut-fundo: #000;
  --c-aut-tinta: #ebebeb;
  --c-aut-tinta-2: #a8a8a8;
  --c-aut-linha: rgba(235, 235, 235, .14);
  --c-aut-midia-fundo: #0a0a0a;
  --c-aut-veu: linear-gradient(to top, rgba(0, 0, 0, .28), transparent 45%);
  --c-aut-corte: 1024px;            /* documental: o media query abaixo não lê variável */
  --c-aut-destaque: #e8ad32;

  background: var(--c-aut-fundo);
  color: var(--c-aut-tinta);
}
.c-login__cabeca { margin: 0 auto 24px; max-width: 300px; }
.c-login__cabeca h1 { font-size: clamp(2rem, 7vw, 2.6rem); margin: 0 0 8px; line-height: 1.15; letter-spacing: -.01em; }
.c-login__cabeca p { margin: 0; color: var(--c-aut-tinta-2); font-size: .95rem; }
.c-login__form { padding: 32px 24px; }
.c-login__midia { display: none; }

/* a entrada escalonada dos campos: 80 ms entre os filhos do form, teto no sétimo */
@media (prefers-reduced-motion: no-preference) {
  .c-login__cabeca, .c-login__form form > * { animation: c-login-entra .5s cubic-bezier(.16, 1, .3, 1) both; }
  .c-login__form form > :nth-child(3) { animation-delay: .08s; }
  .c-login__form form > :nth-child(4) { animation-delay: .16s; }
  .c-login__form form > :nth-child(5) { animation-delay: .24s; }
  .c-login__form form > :nth-child(6) { animation-delay: .32s; }
  .c-login__form form > :nth-child(n+7) { animation-delay: .4s; }
}
@keyframes c-login-entra { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }

/* no desktop: formulário à esquerda, mídia à direita */
@media (min-width: 1024px) {
  .c-login { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); min-height: 100vh; }
  .c-login__form { display: flex; align-items: center; justify-content: center; padding: 64px 48px; }
  .c-login__form > * { width: 100%; max-width: 420px; }
  .c-login__cabeca { max-width: none; margin: 0 0 32px; }
  .c-login__cabeca h1 { font-size: clamp(2.4rem, 3.2vw, 3rem); }
  .c-login__midia { display: block; position: relative; overflow: hidden; background: var(--c-aut-midia-fundo); border-left: 1px solid var(--c-aut-linha); }
  .c-login__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 58% 50%; }
  .c-login__veu { position: absolute; inset: 0; background: var(--c-aut-veu); }
}

/* campos de exemplo — troque pelos do seu hospedeiro */
.c-login__campo { display: grid; gap: 6px; margin-bottom: 16px; }
.c-login__campo label { font-size: .85rem; color: var(--c-aut-tinta-2); }
.c-login__campo input {
  height: 44px; padding: 0 12px;
  border: 1px solid var(--c-aut-linha); border-radius: 10px;
  background: transparent; color: var(--c-aut-tinta); font: inherit;
}
.c-login__campo input:focus-visible { outline: 2px solid var(--c-aut-destaque); outline-offset: 2px; }
.c-login__linha { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; font-size: .85rem; color: var(--c-aut-tinta-2); }
.c-login__linha a { color: var(--c-aut-destaque); }
.c-login__rodape { margin-top: 20px; font-size: .85rem; color: var(--c-aut-tinta-2); text-align: center; }


/* ===== login-dividido.js =========================================================== */

/* AUT-01 · O painel de mídia só carrega onde aparece.
   O vídeo só ganha `src` a partir de 1024 px — abaixo disso o painel está em display:none e o
   celular não baixa nada. Com "reduzir movimento" fica só o poster, sem vídeo.
   O `data-src` e o `data-poster` do markup é que viram `src` e `poster` aqui. */
(function () {
  'use strict';

  function iniciar() {
    var v = document.querySelector('.c-login__video');
    if (!v || !window.matchMedia) { return; }
    var largo = window.matchMedia('(min-width: 1024px)');
    var quieto = window.matchMedia('(prefers-reduced-motion: reduce)');

    function carregar() {
      if (!largo.matches || v.getAttribute('poster')) { return; }
      v.setAttribute('poster', v.getAttribute('data-poster') || '');
      if (quieto.matches) { return; }
      v.setAttribute('src', v.getAttribute('data-src') || '');
      v.muted = true;
      var p = v.play();
      if (p !== undefined) { p.catch(function () { /* o navegador recusou o autoplay: fica o poster */ }); }
    }

    carregar();
    if (largo.addEventListener) { largo.addEventListener('change', carregar); }
  }

  if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', iniciar); } else { iniciar(); }
})();


/* ===== markup.html ================================================================= */

<!-- AUT-01 · Login em tela dividida. O invólucro é o componente; o <form> é o do seu hospedeiro.
     O painel de mídia é decorativo (aria-hidden) e só existe a partir de 1024 px.
     O vídeo nasce sem src: quem o põe é o JS, e só na largura em que o painel aparece. -->
<div class="c-login">
  <div class="c-login__form">
    <div>
      <div class="c-login__cabeca">
        <h1>Entrar</h1>
        <p>Use o e-mail da sua reserva.</p>
      </div>
      <form action="#" method="post">
        <div class="c-login__campo">
          <label for="login-email">E-mail</label>
          <input id="login-email" name="email" type="email" autocomplete="email" required>
        </div>
        <div class="c-login__campo">
          <label for="login-senha">Senha</label>
          <input id="login-senha" name="senha" type="password" autocomplete="current-password" required>
        </div>
        <div class="c-login__linha">
          <label><input type="checkbox" name="lembrar"> Lembrar de mim</label>
          <a href="#">Esqueci a senha</a>
        </div>
        <button type="submit" class="c-botao-origem c-botao-origem--solido" style="width:100%">
          <span class="c-botao-origem__circulo" aria-hidden="true"></span>
          <span class="c-botao-origem__rotulo">Entrar</span>
        </button>
      </form>
      <p class="c-login__rodape">Não tem conta? <a href="#">Criar conta</a></p>
    </div>
  </div>

  <div class="c-login__midia" aria-hidden="true">
    <video class="c-login__video" muted loop playsinline preload="none"
           data-src="hero.mp4" data-poster="hero-poster.jpg"></video>
    <div class="c-login__veu"></div>
  </div>
</div>

Já rodou em

  • trento-pedro · telas do tema no Odoo — login, cadastro e redefinição de senha no desktop, com o vídeo do hero no lugar da imagem (25/09/2026)

Exige

  • react
  • react-hook-form
  • zod
  • @hookform/resolvers/zod
  • framer-motion
  • shadcn/ui (Button, Form*, Input, Checkbox)
  • lucide-react (Loader2)

Como se configura

  • --c-aut-fundo
  • --c-aut-tinta
  • --c-aut-tinta-2
  • --c-aut-linha
  • --c-aut-midia-fundo
  • --c-aut-veu
  • --c-aut-corte

A ficha

O que o 21st.dev entregou

Recebido em 25/09/2026, na rodada de correções das demais telas (quadro do Rafa no Canva correcoes-demais-telas: "fazer uma tela de login adaptada pra desktop"). É o primeiro componente desta pasta que não vai para a home: vai para as telas do tema do Odoo (/web/login, /web/signup, /web/reset_password).

Dependências: React, react-hook-form + @hookform/resolvers/zod + zod (validação no cliente), Framer Motion (motion.div, variantes), primitivos do shadcn/ui (Button, Form*, Input, Checkbox), lucide-react (Loader2), Tailwind. "use client" é do Next.js.

Como funciona:

  • Contêiner min-h-screen, flex-col no celular e md:flex-row a partir de 768 px.
  • Esquerda (md:w-1/2, p-8, centrada): coluna max-w-md com logo, título text-2xl semibold, descrição text-sm apagada, e-mail, senha, linha com "Remember Me" à esquerda e "Forgotten Password" à direita, botão largura total com spinner enquanto envia, e "Don't have an account? Create one here." embaixo.
  • Direita (hidden md:block): a imagem em object-cover com um véu from-black/20 de baixo para cima.
  • Movimento: o contêiner revela os filhos com staggerChildren: 0.1; cada item sobe 20 px com fade.

O que entrou na Trento, e como (25/09/2026):

  • O corte, não o código. O Odoo desenha o formulário dele (com CSRF, captcha, "chave de acesso", mensagens de erro); o que se reaproveitou foi o layout dividido, o título com descrição e a entrada escalonada. Uma view no web.login_layout (odoo/views/login-layout-trento.arch.xml) serve às três telas.
  • Imagem → vídeo do hero da home (decisão do Rafa): o 16:9 (hero-loop-amplo.mp4), porque cortado para meia tela ele mantém a modelo inteira; o vertical cortaria cabeça ou pé. Só ganha src a partir de 1024 px, e com "menos movimento" fica o poster.
  • A partir de 1024 px, não de 768 (o md do original): é onde o layout de tablet e desktop da casa muda. Abaixo disso o celular fica como estava, com o título novo em cima.
  • Entre o cabeçalho e o rodapé do site, não em tela cheia: o visitante continua com a navegação.
  • Saíram: "Remember Me" (o Odoo já mantém a sessão), a validação zod (quem valida é o servidor) e o logo dentro do formulário (o cabeçalho já tem o símbolo).
  • Títulos e textos em pt-BR, tokens da Trento (--tr-tinta-2 para a descrição, CTA em degradê), e o escalonamento em CSS (tr-auth-entra, 80 ms entre filhos) em vez de Framer Motion.

O que a adaptação da OCTA mudou

  • O painel de mídia só ganha src a partir de 1024 px — o celular não baixa nada
  • Com prefers-reduced-motion fica só o poster, sem vídeo

Lacunas do original

  • A validação do original é zod + react-hook-form: sai inteira na adaptação, e com ela a mensagem de erro por campo
  • A imagem do original é decorativa e não tem tratamento de falha de carregamento

A adaptação da OCTA entrega o invólucro dividido e o painel de mídia, não o formulário: no caso da Trento o formulário é o do Odoo. O demo mostra um formulário de exemplo, que não valida nada.

⚠️ Lista incompleta: este componente não passou por apuração de acessibilidade. O que está acima é o que se observou de passagem, não um laudo.