Ir para o conteúdo
nella

Criações

Exemplos que funcionam.

Cada exemplo é uma página real em HTML, CSS e JavaScript. Ele roda aqui, em um quadro isolado, como uma criação de Nella. Clique, digite, jogue.

Estes exemplos foram preparados para esta página para mostrar o tipo de resultado obtido. São fictícios e estão escritos em francês.

Calculadora de gorjeta Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calculateur de pourboire</title>
<style>
  :root{--ink:#0f1f1a;--muted:#5b6b65;--teal:#0f766e;--teal-soft:#e6f4f1;--line:#dfe7e4;--bg:#f6f8f7}
  *{box-sizing:border-box}
  body{margin:0;min-height:100vh;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg);display:grid;place-items:center;padding:28px 16px}
  .card{width:100%;max-width:820px;background:#fff;border:1px solid var(--line);border-radius:24px;display:grid;grid-template-columns:1.1fr 1fr;overflow:hidden;box-shadow:0 30px 60px -40px rgba(15,31,26,.35)}
  .form{padding:28px;display:grid;gap:20px;align-content:start}
  h1{margin:0;font-size:22px;letter-spacing:-.02em}
  .hint{margin:4px 0 0;color:var(--muted);font-size:14px}
  label,.label{display:block;font-size:13px;font-weight:600;color:var(--muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:.06em}
  .money{position:relative}
  .money input{width:100%;font:600 28px/1 system-ui,sans-serif;padding:14px 44px 14px 16px;border:1px solid var(--line);border-radius:14px;background:var(--bg);color:var(--ink);font-variant-numeric:tabular-nums}
  .money span{position:absolute;right:16px;top:50%;transform:translateY(-50%);font-size:22px;color:var(--muted)}
  input:focus,button:focus-visible{outline:2px solid var(--teal);outline-offset:2px}
  .chips{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
  .chips button{border:1px solid var(--line);background:#fff;border-radius:12px;padding:11px 0;font:600 15px system-ui,sans-serif;color:var(--ink);cursor:pointer}
  .chips button[aria-pressed="true"]{background:var(--teal);border-color:var(--teal);color:#fff}
  .custom{display:none;margin-top:8px}
  .custom.on{display:flex;align-items:center;gap:8px}
  .custom input{width:90px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;font:600 16px system-ui,sans-serif}
  .switch{display:flex;gap:6px;background:var(--bg);border:1px solid var(--line);border-radius:12px;padding:4px}
  .switch button{flex:1;border:0;background:none;border-radius:9px;padding:9px 6px;font:600 14px system-ui,sans-serif;color:var(--muted);cursor:pointer}
  .switch button[aria-pressed="true"]{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.08)}
  .people{display:flex;align-items:center;gap:14px}
  .people button{width:40px;height:40px;border-radius:12px;border:1px solid var(--line);background:#fff;font:600 20px/1 system-ui,sans-serif;cursor:pointer;color:var(--ink)}
  .people output{font:700 22px system-ui,sans-serif;min-width:2ch;text-align:center;font-variant-numeric:tabular-nums}
  .result{background:var(--ink);color:#e8f1ee;padding:28px;display:flex;flex-direction:column;gap:6px}
  .line{display:flex;justify-content:space-between;font-size:15px;color:#a9bbb4;padding:7px 0;border-bottom:1px solid rgba(255,255,255,.08);font-variant-numeric:tabular-nums}
  .line b{color:#e8f1ee;font-weight:600}
  .line.total{color:#fff;font-size:17px;border:0;padding-top:12px}
  .line.total b{font-size:20px}
  .each{margin-top:auto;padding-top:22px}
  .each small{display:block;color:#7fd1c4;font-size:13px;text-transform:uppercase;letter-spacing:.08em;font-weight:600}
  .each strong{display:block;font-size:46px;letter-spacing:-.03em;line-height:1.1;font-variant-numeric:tabular-nums}
  .each p{margin:6px 0 0;font-size:13px;color:#a9bbb4}
  @media (max-width:680px){.card{grid-template-columns:1fr}.each strong{font-size:38px}}
</style>
</head>
<body>
<main class="card">
  <div class="form">
    <div>
      <h1>Calculateur de pourboire</h1>
      <p class="hint">Avec la TPS (5 %) et la TVQ (9,975 %) du Québec.</p>
    </div>

    <div>
      <label for="amount">Montant avant taxes</label>
      <div class="money"><input id="amount" type="text" inputmode="decimal" value="84,50" autocomplete="off"><span>$</span></div>
    </div>

    <div>
      <span class="label" id="tip-label">Pourboire</span>
      <div class="chips" role="group" aria-labelledby="tip-label">
        <button type="button" data-tip="15" aria-pressed="false">15 %</button>
        <button type="button" data-tip="18" aria-pressed="true">18 %</button>
        <button type="button" data-tip="20" aria-pressed="false">20 %</button>
        <button type="button" data-tip="autre" aria-pressed="false">Autre</button>
      </div>
      <div class="custom" id="custom">
        <input id="customTip" type="number" min="0" max="100" step="0.5" value="22" aria-label="Pourboire personnalisé en pourcentage"> <span>%</span>
      </div>
    </div>

    <div>
      <span class="label" id="base-label">Pourboire calculé sur</span>
      <div class="switch" role="group" aria-labelledby="base-label">
        <button type="button" data-base="avant" aria-pressed="true">Avant taxes</button>
        <button type="button" data-base="apres" aria-pressed="false">Après taxes</button>
      </div>
    </div>

    <div>
      <span class="label">Nombre de personnes</span>
      <div class="people">
        <button type="button" id="less" aria-label="Une personne de moins">−</button>
        <output id="people" aria-live="polite">2</output>
        <button type="button" id="more" aria-label="Une personne de plus">+</button>
      </div>
    </div>
  </div>

  <div class="result" aria-live="polite">
    <div class="line"><span>Sous-total</span><b id="sub">0,00 $</b></div>
    <div class="line"><span>TPS (5 %)</span><b id="tps">0,00 $</b></div>
    <div class="line"><span>TVQ (9,975 %)</span><b id="tvq">0,00 $</b></div>
    <div class="line"><span>Pourboire (<span id="pct">18</span> %)</span><b id="tip">0,00 $</b></div>
    <div class="line total"><span>Total</span><b id="total">0,00 $</b></div>
    <div class="each">
      <small>Par personne</small>
      <strong id="each">0,00 $</strong>
      <p id="split">Partagé entre 2 personnes.</p>
    </div>
  </div>
</main>

<script>
  const money = new Intl.NumberFormat('fr-CA', { style: 'currency', currency: 'CAD' });
  const cents = n => Math.round(n * 100) / 100;
  const state = { tip: 18, custom: false, base: 'avant', people: 2 };
  const $ = id => document.getElementById(id);

  function amount() {
    const n = parseFloat($('amount').value.replace(/\s/g, '').replace(',', '.'));
    return Number.isFinite(n) && n > 0 ? n : 0;
  }

  function update() {
    const sub = cents(amount());
    const tps = cents(sub * 0.05);
    const tvq = cents(sub * 0.09975);
    const pct = state.custom ? Math.max(0, parseFloat($('customTip').value) || 0) : state.tip;
    const base = state.base === 'avant' ? sub : sub + tps + tvq;
    const tip = cents(base * pct / 100);
    const total = cents(sub + tps + tvq + tip);
    $('sub').textContent = money.format(sub);
    $('tps').textContent = money.format(tps);
    $('tvq').textContent = money.format(tvq);
    $('pct').textContent = String(pct).replace('.', ',');
    $('tip').textContent = money.format(tip);
    $('total').textContent = money.format(total);
    $('each').textContent = money.format(Math.ceil(total / state.people * 100) / 100);
    $('people').textContent = state.people;
    $('split').textContent = state.people === 1
      ? 'Une seule personne paie.'
      : 'Partagé entre ' + state.people + ' personnes, arrondi au cent supérieur.';
  }

  document.querySelectorAll('[data-tip]').forEach(b => b.addEventListener('click', () => {
    document.querySelectorAll('[data-tip]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
    state.custom = b.dataset.tip === 'autre';
    if (!state.custom) state.tip = Number(b.dataset.tip);
    $('custom').classList.toggle('on', state.custom);
    if (state.custom) $('customTip').focus();
    update();
  }));
  document.querySelectorAll('[data-base]').forEach(b => b.addEventListener('click', () => {
    document.querySelectorAll('[data-base]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
    state.base = b.dataset.base;
    update();
  }));
  $('less').addEventListener('click', () => { state.people = Math.max(1, state.people - 1); update(); });
  $('more').addEventListener('click', () => { state.people = Math.min(20, state.people + 1); update(); });
  $('amount').addEventListener('input', update);
  $('customTip').addEventListener('input', update);
  update();
</script>
</body>
</html>

Aplicativo

Calculadora de gorjeta

O pedido

“Uma calculadora de gorjeta para Quebec: impostos federal e provincial, porcentagem à escolha e divisão entre várias pessoas.”

O que funciona

  • Impostos de 5% e de 9,975%, arredondados ao centavo.
  • Gorjeta calculada antes ou depois dos impostos.
  • Divisão da conta, arredondada ao centavo acima.
Mie & Levain Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mie & Levain · Boulangerie</title>
<style>
  :root{--ink:#1f1b16;--muted:#6d665c;--green:#22443a;--gold:#c98f2b;--soft:#f4f2ee;--line:#e7e3dc}
  *{box-sizing:border-box}
  html{scroll-behavior:smooth}
  body{margin:0;font:16px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:#fff}
  h1,h2,.logo{font-family:Georgia,"Times New Roman",serif;font-weight:400;letter-spacing:-.01em}
  a{color:inherit}
  .top{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:20px;padding:14px 6vw;background:rgba(255,255,255,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
  .logo{font-size:22px;text-decoration:none}
  .logo i{color:var(--gold)}
  nav{display:flex;gap:18px;margin-left:auto;font-size:14px}
  nav a{text-decoration:none;color:var(--muted)}
  nav a:hover{color:var(--ink)}
  .status{font-size:12.5px;font-weight:600;padding:5px 10px;border-radius:99px;background:var(--soft);white-space:nowrap}
  .status.open{background:#e3f1e8;color:#1d6b43}
  .status.closed{background:#f6e7e2;color:#9a3b22}
  .hero{display:grid;grid-template-columns:1.1fr .9fr;gap:4vw;align-items:center;padding:56px 6vw 48px}
  .kicker{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:var(--gold);font-weight:600;margin:0 0 12px}
  h1{font-size:clamp(34px,5vw,56px);line-height:1.05;margin:0 0 16px}
  .hero p{color:var(--muted);max-width:42ch;margin:0 0 24px}
  .btn{display:inline-block;background:var(--green);color:#fff;text-decoration:none;padding:12px 20px;border-radius:999px;font-weight:600;font-size:15px;border:0;cursor:pointer}
  .btn:hover{background:#1a362d}
  .hero svg{width:100%;height:auto;max-height:280px}
  section{padding:40px 6vw}
  h2{font-size:34px;margin:0 0 6px}
  .sub{color:var(--muted);margin:0 0 22px}
  .filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px}
  .filters button{border:1px solid var(--line);background:#fff;border-radius:999px;padding:7px 14px;font:inherit;font-size:14px;cursor:pointer}
  .filters button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:#fff}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px}
  .item{border:1px solid var(--line);border-radius:16px;padding:16px;display:flex;flex-direction:column;gap:6px}
  .item b{font-size:17px}
  .item span{color:var(--muted);font-size:14px;flex:1}
  .row{display:flex;align-items:center;justify-content:space-between;margin-top:8px}
  .price{font-weight:700;font-variant-numeric:tabular-nums}
  .add{border:0;background:var(--soft);border-radius:999px;padding:6px 12px;font:inherit;font-size:13.5px;font-weight:600;cursor:pointer}
  .add:hover{background:#ebe7df}
  .basket{margin-top:18px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:14px 16px;border-radius:14px;background:var(--soft);font-size:14.5px}
  .basket b{font-variant-numeric:tabular-nums}
  .basket .btn{padding:9px 16px;font-size:14px;margin-left:auto}
  .basket .btn:disabled{opacity:.45;cursor:default}
  #note{width:100%;margin:0;color:var(--green);font-weight:600}
  table{width:100%;max-width:460px;border-collapse:collapse;font-size:15px}
  td{padding:10px 0;border-bottom:1px solid var(--line)}
  td:last-child{text-align:right;font-variant-numeric:tabular-nums}
  tr.today td{font-weight:700;color:var(--green)}
  footer{padding:28px 6vw 36px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--line)}
  @media (max-width:640px){.hero{grid-template-columns:1fr;padding-top:36px}.hero svg{max-height:180px}nav{display:none}.status{margin-left:auto}}
</style>
</head>
<body>
<header class="top">
  <a class="logo" href="#accueil">Mie <i>&amp;</i> Levain</a>
  <nav><a href="#pains">Nos pains</a><a href="#horaire">Horaire</a></nav>
  <span class="status" id="status">Horaire</span>
</header>

<section class="hero" id="accueil">
  <div>
    <p class="kicker">Boulangerie de quartier · Montréal</p>
    <h1>Le pain du quartier, cuit chaque matin.</h1>
    <p>Farines moulues au Québec, levain naturel et fermentation lente. Première fournée à 7 h.</p>
    <a class="btn" href="#pains">Voir les pains du jour</a>
  </div>
  <svg viewBox="0 0 320 220" aria-hidden="true">
    <ellipse cx="160" cy="196" rx="132" ry="12" fill="#f1ede6"/>
    <path d="M38 150c0-58 54-98 122-98s122 40 122 98c0 26-20 40-52 40H90c-32 0-52-14-52-40Z" fill="#d79a3c"/>
    <path d="M52 146c4-46 50-78 108-78s104 32 108 78" fill="none" stroke="#e9b864" stroke-width="10" stroke-linecap="round" opacity=".7"/>
    <g fill="none" stroke="#a8691f" stroke-width="7" stroke-linecap="round">
      <path d="M92 118c14-16 30-22 46-22"/><path d="M140 104c14-14 30-18 46-16"/><path d="M190 100c14-8 28-8 40 0"/>
    </g>
    <circle cx="268" cy="54" r="10" fill="#c98f2b" opacity=".35"/><circle cx="286" cy="80" r="5" fill="#c98f2b" opacity=".25"/>
  </svg>
</section>

<section id="pains">
  <h2>Nos pains</h2>
  <p class="sub">Réservez aujourd’hui, passez les prendre demain matin.</p>
  <div class="filters" role="group" aria-label="Filtrer">
    <button type="button" data-f="tout" aria-pressed="true">Tout</button>
    <button type="button" data-f="pain" aria-pressed="false">Pains</button>
    <button type="button" data-f="vien" aria-pressed="false">Viennoiseries</button>
  </div>
  <div class="grid" id="grid"></div>
  <div class="basket">
    <span>Ma réservation : <b id="count">0 article</b> · <b id="total">0,00 $</b></span>
    <button class="btn" type="button" id="book" disabled>Réserver</button>
    <p id="note" hidden></p>
  </div>
</section>

<section id="horaire">
  <h2>Horaire</h2>
  <p class="sub">Fermé le lundi. La journée en cours est en vert.</p>
  <table><tbody id="hours"></tbody></table>
</section>

<footer>Mie &amp; Levain · Plateau-Mont-Royal, Montréal · Boulangerie fictive créée pour un exemple.</footer>

<script>
  const products = [
    { name: 'Campagne au levain', desc: 'Croûte épaisse, mie alvéolée.', price: 6.5, cat: 'pain' },
    { name: 'Baguette tradition', desc: 'Pétrie lentement, cuite deux fois par jour.', price: 3.5, cat: 'pain' },
    { name: 'Seigle et noix', desc: 'Dense, parfait avec les fromages.', price: 7.25, cat: 'pain' },
    { name: 'Croissant au beurre', desc: 'Feuilleté sur trois jours.', price: 2.75, cat: 'vien' },
    { name: 'Chocolatine', desc: 'Deux barres de chocolat noir.', price: 3.25, cat: 'vien' },
    { name: 'Brioche à l’érable', desc: 'Glacée au sirop d’érable.', price: 4.0, cat: 'vien' },
  ];
  const hours = [
    ['Dimanche', '8 h à 16 h', 8, 16], ['Lundi', 'Fermé', 0, 0], ['Mardi', '7 h à 18 h', 7, 18],
    ['Mercredi', '7 h à 18 h', 7, 18], ['Jeudi', '7 h à 18 h', 7, 18], ['Vendredi', '7 h à 19 h', 7, 19],
    ['Samedi', '7 h à 17 h', 7, 17],
  ];
  const money = new Intl.NumberFormat('fr-CA', { style: 'currency', currency: 'CAD' });
  const basket = [];

  function renderGrid(filter) {
    const grid = document.getElementById('grid');
    grid.innerHTML = '';
    products.filter(p => filter === 'tout' || p.cat === filter).forEach(p => {
      const el = document.createElement('article');
      el.className = 'item';
      el.innerHTML = `<b>${p.name}</b><span>${p.desc}</span>
        <div class="row"><span class="price">${money.format(p.price)}</span>
        <button class="add" type="button">Ajouter</button></div>`;
      el.querySelector('.add').addEventListener('click', () => { basket.push(p); renderBasket(); });
      grid.appendChild(el);
    });
  }

  function renderBasket() {
    const n = basket.length;
    document.getElementById('count').textContent = n + (n > 1 ? ' articles' : ' article');
    document.getElementById('total').textContent = money.format(basket.reduce((s, p) => s + p.price, 0));
    document.getElementById('book').disabled = n === 0;
  }

  document.querySelectorAll('.filters button').forEach(b => b.addEventListener('click', () => {
    document.querySelectorAll('.filters button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
    renderGrid(b.dataset.f);
  }));

  document.getElementById('book').addEventListener('click', () => {
    const note = document.getElementById('note');
    note.textContent = 'Merci ! Votre réservation de ' + basket.length + ' article(s) vous attend demain dès 7 h.';
    note.hidden = false;
    basket.length = 0;
    renderBasket();
  });

  const now = new Date();
  const tbody = document.getElementById('hours');
  [2, 3, 4, 5, 6, 0, 1].forEach(d => {
    const tr = document.createElement('tr');
    if (d === now.getDay()) tr.className = 'today';
    tr.innerHTML = `<td>${hours[d][0]}</td><td>${hours[d][1]}</td>`;
    tbody.appendChild(tr);
  });
  const [, , open, close] = hours[now.getDay()];
  const h = now.getHours() + now.getMinutes() / 60;
  const status = document.getElementById('status');
  const isOpen = h >= open && h < close;
  status.textContent = isOpen ? 'Ouvert maintenant' : 'Fermé pour le moment';
  status.className = 'status ' + (isOpen ? 'open' : 'closed');

  document.querySelectorAll('a[href^="#"]').forEach(a => a.addEventListener('click', e => {
    e.preventDefault();
    document.querySelector(a.getAttribute('href')).scrollIntoView({ behavior: 'smooth' });
  }));

  renderGrid('tout');
  renderBasket();
</script>
</body>
</html>

Site institucional

Mie & Levain

O pedido

“O site da minha padaria: nossos pães, nossos preços, o horário e reservas para o dia seguinte.”

O que funciona

  • Filtro de produtos.
  • Reserva com total em dólares.
  • Indicação “Aberto agora” conforme a hora real.
Paires Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Paires · Jeu de mémoire</title>
<style>
  :root{--ink:#18181b;--muted:#71717a;--bg:#f4f4f5;--card:#27272a;--line:#e4e4e7}
  *{box-sizing:border-box}
  body{margin:0;min-height:100vh;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg);display:grid;place-items:center;padding:24px 14px}
  .game{width:100%;max-width:440px}
  header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:16px}
  h1{margin:0;font-size:28px;letter-spacing:-.03em}
  h1 span{display:block;font-size:13px;font-weight:500;color:var(--muted);letter-spacing:0}
  .stats{display:flex;gap:8px}
  .stat{background:#fff;border:1px solid var(--line);border-radius:12px;padding:6px 12px;text-align:center;min-width:72px}
  .stat small{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
  .stat b{font-variant-numeric:tabular-nums;font-size:18px}
  .board{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;perspective:800px}
  .card{aspect-ratio:1;border:0;padding:0;background:none;cursor:pointer;position:relative;transform-style:preserve-3d;transition:transform .35s ease;border-radius:14px}
  .card:focus-visible{outline:3px solid #f97316;outline-offset:3px}
  .card.up,.card.done{transform:rotateY(180deg)}
  .face{position:absolute;inset:0;border-radius:14px;display:grid;place-items:center;backface-visibility:hidden}
  .back{background:var(--card);background-image:radial-gradient(circle at 30% 30%,#3f3f46 0 2px,transparent 3px);background-size:14px 14px}
  .front{background:#fff;border:1px solid var(--line);transform:rotateY(180deg)}
  .front svg{width:56%;height:56%}
  .card.done .front{background:#f0fdf4;border-color:#bbf7d0}
  .bar{display:flex;justify-content:space-between;align-items:center;margin-top:16px;gap:12px}
  .bar p{margin:0;color:var(--muted);font-size:14px}
  button.new{border:0;background:var(--ink);color:#fff;border-radius:999px;padding:10px 18px;font:600 14px system-ui,sans-serif;cursor:pointer}
  .win{margin-top:14px;padding:14px 16px;border-radius:14px;background:#fff7ed;border:1px solid #fed7aa;font-size:15px}
  @media (prefers-reduced-motion:reduce){.card{transition:none}}
</style>
</head>
<body>
<main class="game">
  <header>
    <h1>Paires <span>Retrouvez les 8 paires de formes.</span></h1>
    <div class="stats">
      <div class="stat"><small>Coups</small><b id="moves">0</b></div>
      <div class="stat"><small>Temps</small><b id="time">0:00</b></div>
    </div>
  </header>
  <div class="board" id="board" aria-label="Plateau de jeu"></div>
  <div class="win" id="win" role="status" hidden></div>
  <div class="bar">
    <p id="best">Meilleur score : aucun pour l’instant</p>
    <button class="new" type="button" id="restart">Nouvelle partie</button>
  </div>
</main>

<script>
  const shapes = {
    rond: ['Rond', '<circle cx="12" cy="12" r="9" fill="#f97316"/>'],
    carre: ['Carré', '<rect x="4" y="4" width="16" height="16" rx="3" fill="#0ea5e9"/>'],
    triangle: ['Triangle', '<path d="M12 3 22 20H2Z" fill="#22c55e"/>'],
    losange: ['Losange', '<path d="M12 2 21 12 12 22 3 12Z" fill="#e11d48"/>'],
    etoile: ['Étoile', '<path d="m12 2 2.9 6.6 7.1.6-5.4 4.7 1.6 7L12 17.3 5.8 21l1.6-7L2 9.2l7.1-.6Z" fill="#eab308"/>'],
    anneau: ['Anneau', '<circle cx="12" cy="12" r="7.5" fill="none" stroke="#8b5cf6" stroke-width="4"/>'],
    croix: ['Croix', '<path d="M9 3h6v6h6v6h-6v6H9v-6H3V9h6Z" fill="#14b8a6"/>'],
    lune: ['Lune', '<path d="M15 3a9 9 0 1 0 6 15A8 8 0 0 1 15 3Z" fill="#64748b"/>'],
  };
  let first = null, lock = false, moves = 0, found = 0, seconds = 0, timer = null, best = null;
  const $ = id => document.getElementById(id);

  function shuffle(list) {
    for (let i = list.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [list[i], list[j]] = [list[j], list[i]];
    }
    return list;
  }

  function clock() {
    $('time').textContent = Math.floor(seconds / 60) + ':' + String(seconds % 60).padStart(2, '0');
  }

  function start() {
    clearInterval(timer);
    timer = null; first = null; lock = false; moves = 0; found = 0; seconds = 0;
    $('moves').textContent = '0';
    $('win').hidden = true;
    clock();
    const board = $('board');
    board.innerHTML = '';
    shuffle([...Object.keys(shapes), ...Object.keys(shapes)]).forEach(key => {
      const card = document.createElement('button');
      card.type = 'button';
      card.className = 'card';
      card.dataset.key = key;
      card.setAttribute('aria-label', 'Carte cachée');
      card.innerHTML = `<span class="face back"></span><span class="face front"><svg viewBox="0 0 24 24" aria-hidden="true">${shapes[key][1]}</svg></span>`;
      card.addEventListener('click', () => flip(card));
      board.appendChild(card);
    });
  }

  function flip(card) {
    if (lock || card === first || card.classList.contains('done')) return;
    if (!timer) timer = setInterval(() => { seconds++; clock(); }, 1000);
    card.classList.add('up');
    card.setAttribute('aria-label', shapes[card.dataset.key][0]);
    if (!first) { first = card; return; }
    moves++;
    $('moves').textContent = moves;
    if (first.dataset.key === card.dataset.key) {
      first.classList.add('done'); card.classList.add('done');
      first = null; found++;
      if (found === 8) finish();
      return;
    }
    lock = true;
    const a = first, b = card;
    setTimeout(() => {
      a.classList.remove('up'); b.classList.remove('up');
      a.setAttribute('aria-label', 'Carte cachée'); b.setAttribute('aria-label', 'Carte cachée');
      first = null; lock = false;
    }, 750);
  }

  function finish() {
    clearInterval(timer);
    const win = $('win');
    win.textContent = `Bravo ! Toutes les paires en ${moves} coups et ${$('time').textContent}.`;
    win.hidden = false;
    if (best === null || moves < best) {
      best = moves;
      $('best').textContent = 'Meilleur score : ' + best + ' coups';
    }
  }

  $('restart').addEventListener('click', start);
  start();
</script>
</body>
</html>

Jogo

Paires

O pedido

“Um jogo da memória com formas coloridas, um cronômetro e um contador de jogadas.”

O que funciona

  • Cartas embaralhadas a cada partida.
  • Cronômetro e contador de jogadas.
  • Jogável pelo teclado.
Budget du mois Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Budget du mois</title>
<style>
  :root{--ink:#111827;--muted:#6b7280;--line:#e5e7eb;--soft:#f9fafb;--ok:#059669;--bad:#dc2626;--accent:#2563eb}
  *{box-sizing:border-box}
  body{margin:0;font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:#fff;padding:28px 5vw}
  h1{margin:0;font-size:26px;letter-spacing:-.02em}
  .sub{margin:2px 0 22px;color:var(--muted)}
  .layout{display:grid;grid-template-columns:1.25fr 1fr;gap:28px;align-items:start}
  label.income{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;background:var(--soft);border:1px solid var(--line);border-radius:14px;font-weight:600;margin-bottom:16px}
  input{font:inherit;color:inherit}
  .num{width:120px;text-align:right;border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-variant-numeric:tabular-nums;background:#fff}
  .num:focus,.name:focus{outline:2px solid var(--accent);outline-offset:1px}
  table{width:100%;border-collapse:collapse}
  th{text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;padding:0 0 8px}
  th:nth-child(2){text-align:right}
  td{padding:6px 0;border-top:1px solid var(--line)}
  td:nth-child(2){text-align:right}
  .name{width:100%;border:1px solid transparent;border-radius:8px;padding:7px 8px;background:none}
  .name:hover{border-color:var(--line)}
  .del{border:0;background:none;color:var(--muted);font-size:18px;cursor:pointer;width:32px;height:32px;border-radius:8px}
  .del:hover{background:#fef2f2;color:var(--bad)}
  .add{margin-top:10px;border:1px dashed #cbd5e1;background:none;border-radius:10px;padding:9px 14px;font:600 14px system-ui,sans-serif;color:var(--accent);cursor:pointer;width:100%}
  .summary{border:1px solid var(--line);border-radius:18px;padding:20px;display:grid;gap:16px;position:sticky;top:16px}
  .ring{width:150px;height:150px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:conic-gradient(var(--accent) calc(var(--p)*1%),#eef2ff 0)}
  .ring div{width:112px;height:112px;border-radius:50%;background:#fff;display:grid;place-items:center;text-align:center;line-height:1.2}
  .ring b{font-size:24px;font-variant-numeric:tabular-nums}
  .ring small{display:block;color:var(--muted);font-size:12px}
  .totals div{display:flex;justify-content:space-between;padding:6px 0;font-variant-numeric:tabular-nums}
  .left{font-weight:700;font-size:18px}
  .left.ok{color:var(--ok)}.left.bad{color:var(--bad)}
  .bars{display:grid;gap:8px}
  .bar{display:grid;grid-template-columns:110px 1fr 46px;gap:8px;align-items:center;font-size:13px}
  .bar span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .bar i{display:block;height:8px;border-radius:99px;background:var(--accent);min-width:2px}
  .bar em{font-style:normal;color:var(--muted);text-align:right;font-variant-numeric:tabular-nums}
  .tip{margin:0;font-size:13px;color:var(--muted);background:var(--soft);border-radius:12px;padding:10px 12px}
  @media (max-width:720px){.layout{grid-template-columns:1fr}.summary{position:static}}
</style>
</head>
<body>
<h1>Budget du mois</h1>
<p class="sub">Modifiez les montants : tout se recalcule aussitôt.</p>

<div class="layout">
  <section>
    <label class="income">Revenu net du mois <input class="num" id="income" type="number" min="0" step="10" value="4800"></label>
    <table>
      <thead><tr><th>Dépense</th><th>Montant</th><th><span style="position:absolute;left:-9999px">Supprimer</span></th></tr></thead>
      <tbody id="rows"></tbody>
    </table>
    <button class="add" type="button" id="add">+ Ajouter une dépense</button>
  </section>

  <aside class="summary" aria-live="polite">
    <div class="ring" id="ring" style="--p:0"><div><span><b id="pct">0 %</b><small>du revenu dépensé</small></span></div></div>
    <div class="totals">
      <div><span>Dépenses</span><b id="spent">0 $</b></div>
      <div><span>Reste</span><b class="left" id="left">0 $</b></div>
    </div>
    <div class="bars" id="bars"></div>
    <p class="tip" id="tip"></p>
  </aside>
</div>

<script>
  const money = new Intl.NumberFormat('fr-CA', { style: 'currency', currency: 'CAD', maximumFractionDigits: 0 });
  const rows = [
    ['Logement', 1350], ['Épicerie', 620], ['Transport', 180], ['Électricité', 85],
    ['Internet et cellulaire', 120], ['Loisirs', 200], ['Épargne', 500],
  ];
  const $ = id => document.getElementById(id);

  function render() {
    const tbody = $('rows');
    tbody.innerHTML = '';
    rows.forEach((row, i) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `<td><input class="name" aria-label="Nom de la dépense"></td>
        <td><input class="num" type="number" min="0" step="5" aria-label="Montant"></td>
        <td><button class="del" type="button" aria-label="Supprimer">×</button></td>`;
      const [name, amount] = tr.querySelectorAll('input');
      name.value = row[0];
      amount.value = row[1];
      name.addEventListener('input', () => { row[0] = name.value; update(); });
      amount.addEventListener('input', () => { row[1] = Math.max(0, parseFloat(amount.value) || 0); update(); });
      tr.querySelector('.del').addEventListener('click', () => { rows.splice(i, 1); render(); });
      tbody.appendChild(tr);
    });
    update();
  }

  function update() {
    const income = Math.max(0, parseFloat($('income').value) || 0);
    const spent = rows.reduce((s, r) => s + r[1], 0);
    const left = income - spent;
    const pct = income ? Math.round(spent / income * 100) : 0;
    $('spent').textContent = money.format(spent);
    $('left').textContent = money.format(left);
    $('left').className = 'left ' + (left >= 0 ? 'ok' : 'bad');
    $('pct').textContent = pct + ' %';
    $('ring').style.setProperty('--p', Math.min(100, pct));
    const max = Math.max(...rows.map(r => r[1]), 1);
    $('bars').innerHTML = '';
    rows.slice().sort((a, b) => b[1] - a[1]).forEach(r => {
      const div = document.createElement('div');
      div.className = 'bar';
      div.innerHTML = '<span></span><i></i><em></em>';
      div.querySelector('span').textContent = r[0] || 'Sans nom';
      div.querySelector('i').style.width = (r[1] / max * 100) + '%';
      div.querySelector('em').textContent = income ? Math.round(r[1] / income * 100) + ' %' : '–';
      $('bars').appendChild(div);
    });
    const saving = rows.find(r => /pargne/i.test(r[0]));
    const savingPct = saving && income ? Math.round(saving[1] / income * 100) : 0;
    $('tip').textContent = left < 0
      ? 'Vos dépenses dépassent votre revenu de ' + money.format(-left) + '.'
      : 'Vous épargnez ' + savingPct + ' % de votre revenu. Il vous reste ' + money.format(left) + ' à répartir.';
  }

  $('income').addEventListener('input', update);
  $('add').addEventListener('click', () => {
    rows.push(['Nouvelle dépense', 0]);
    render();
    const inputs = document.querySelectorAll('.name');
    inputs[inputs.length - 1].select();
  });
  render();
</script>
</body>
</html>

Planilha

Budget du mois

O pedido

“Uma tabela de orçamento mensal editável, com o saldo restante e um gráfico por categoria.”

O que funciona

  • Linhas editáveis, inclusão e exclusão.
  • Total, saldo e parte da renda.
  • Gráfico recalculado a cada digitação.
Orbite Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orbite · L’agenda qui se range tout seul</title>
<style>
  :root{--bg:#0c0f0d;--ink:#f1f5ef;--muted:#9aa59c;--lime:#c6f432;--line:rgba(241,245,239,.12)}
  *{box-sizing:border-box}
  body{margin:0;font:16px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg)}
  .nav{display:flex;align-items:center;justify-content:space-between;padding:20px 6vw}
  .logo{display:flex;align-items:center;gap:9px;font-weight:700;letter-spacing:-.02em;font-size:19px}
  .logo i{width:20px;height:20px;border-radius:50%;border:3px solid var(--lime);position:relative}
  .logo i::after{content:"";position:absolute;width:6px;height:6px;border-radius:50%;background:var(--lime);top:-6px;right:-5px}
  .tag{font-size:13px;color:var(--muted);border:1px solid var(--line);padding:5px 12px;border-radius:99px}
  .hero{display:grid;grid-template-columns:1.15fr .85fr;gap:5vw;align-items:center;padding:36px 6vw 56px}
  h1{font-size:clamp(36px,5.4vw,64px);line-height:1.02;letter-spacing:-.04em;margin:0 0 18px}
  h1 em{font-style:normal;color:var(--lime)}
  .lead{color:var(--muted);font-size:18px;max-width:40ch;margin:0 0 26px}
  .count{display:flex;gap:10px;margin-bottom:26px}
  .count div{min-width:70px;border:1px solid var(--line);border-radius:14px;padding:10px 12px;text-align:center}
  .count b{display:block;font-size:26px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}
  .count small{color:var(--muted);font-size:12px}
  .join{display:flex;gap:8px;max-width:440px;flex-wrap:wrap}
  .join input{flex:1;min-width:200px;background:rgba(241,245,239,.06);border:1px solid var(--line);border-radius:12px;padding:13px 14px;color:var(--ink);font:inherit}
  .join input:focus{outline:2px solid var(--lime);outline-offset:1px}
  .join button{border:0;background:var(--lime);color:#0c0f0d;font:700 15px system-ui,sans-serif;border-radius:12px;padding:13px 18px;cursor:pointer}
  #msg{min-height:24px;margin:10px 0 0;font-size:14px;color:var(--lime)}
  #msg.err{color:#ff8a7a}
  .phone{justify-self:center;width:250px;height:500px;border-radius:40px;border:8px solid #1f2420;background:#141815;padding:22px 16px;display:flex;flex-direction:column;gap:10px;box-shadow:0 40px 80px -30px rgba(198,244,50,.25)}
  .day{font-size:13px;color:var(--muted);display:flex;justify-content:space-between}
  .day b{color:var(--ink);font-size:20px}
  .ev{border-radius:14px;padding:10px 12px;font-size:13px;line-height:1.35}
  .ev small{display:block;opacity:.7}
  .ev.a{background:var(--lime);color:#0c0f0d}
  .ev.b{background:#232a24}
  .ev.c{background:#2b2f1c;border:1px dashed rgba(198,244,50,.5)}
  .features{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);border-top:1px solid var(--line)}
  .features div{background:var(--bg);padding:28px 6vw 34px 28px}
  .features h2{font-size:17px;margin:0 0 6px}
  .features p{margin:0;color:var(--muted);font-size:14.5px}
  footer{padding:22px 6vw;color:var(--muted);font-size:13px;border-top:1px solid var(--line)}
  @media (max-width:720px){.hero{grid-template-columns:1fr}.phone{display:none}.features{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="nav"><span class="logo"><i></i>Orbite</span><span class="tag">Bêta privée</span></div>

<header class="hero">
  <div>
    <h1>Votre semaine, <em>rangée toute seule.</em></h1>
    <p class="lead">Orbite place vos tâches entre vos rendez-vous, au bon moment, et les déplace quand la journée change.</p>
    <div class="count" aria-label="Temps avant le lancement">
      <div><b id="d">0</b><small>jours</small></div>
      <div><b id="h">0</b><small>heures</small></div>
      <div><b id="m">0</b><small>minutes</small></div>
      <div><b id="s">0</b><small>secondes</small></div>
    </div>
    <div class="join">
      <input id="email" type="email" placeholder="[email protected]" aria-label="Adresse courriel">
      <button type="button" id="join">Rejoindre la liste</button>
    </div>
    <p id="msg" role="status"></p>
  </div>
  <div class="phone" aria-hidden="true">
    <div class="day"><span>Mardi</span><b>14</b></div>
    <div class="ev b">9 h 00 · Réunion d’équipe<small>Salle 2</small></div>
    <div class="ev a">10 h 00 · Préparer la présentation<small>Placée par Orbite · 90 min</small></div>
    <div class="ev b">12 h 00 · Dîner avec Léa</div>
    <div class="ev c">14 h 30 · Répondre aux courriels<small>Déplacée : la réunion a débordé</small></div>
    <div class="ev b">16 h 00 · Appel client</div>
  </div>
</header>

<section class="features">
  <div><h2>Planification automatique</h2><p>Indiquez la durée d’une tâche : Orbite lui trouve une place.</p></div>
  <div><h2>Journées qui s’adaptent</h2><p>Un imprévu ? Le reste de la journée se réorganise.</p></div>
  <div><h2>Vos agendas réunis</h2><p>Travail et personnel, dans une seule vue.</p></div>
</section>

<footer>Orbite est une application fictive, créée pour un exemple.</footer>

<script>
  const launch = new Date('2027-03-01T09:00:00-05:00');
  const $ = id => document.getElementById(id);

  function tick() {
    let left = Math.max(0, launch - new Date());
    const parts = [86400000, 3600000, 60000, 1000].map(unit => {
      const value = Math.floor(left / unit);
      left -= value * unit;
      return value;
    });
    ['d', 'h', 'm', 's'].forEach((id, i) => { $(id).textContent = parts[i]; });
  }

  function join() {
    const email = $('email').value.trim();
    const msg = $('msg');
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      msg.textContent = 'Entrez une adresse courriel valide.';
      msg.className = 'err';
      return;
    }
    msg.textContent = 'Merci ! Vous êtes sur la liste. (Exemple : aucune adresse n’est envoyée.)';
    msg.className = '';
    $('email').value = '';
  }

  $('join').addEventListener('click', join);
  $('email').addEventListener('keydown', e => { if (e.key === 'Enter') join(); });
  tick();
  setInterval(tick, 1000);
</script>
</body>
</html>

Página de lançamento

Orbite

O pedido

“Uma página de lançamento para um app de agenda, com contagem regressiva e lista de espera.”

O que funciona

  • Contagem regressiva ao vivo.
  • Validação do endereço de e-mail.
  • Layout escuro, adaptado ao celular.
Focus Exemplo em francês
Código Pré-visualização
<!doctype html>
<html lang="fr-CA">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Focus · Minuteur de concentration</title>
<style>
  :root{--ink:#2a1215;--muted:#8a6b6e;--red:#e5484d;--soft:#fff1f1;--green:#1f8a5b;--green-soft:#e9f7ef}
  *{box-sizing:border-box}
  body{margin:0;min-height:100vh;display:grid;place-items:center;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--soft);transition:background .4s}
  body.pause{background:var(--green-soft)}
  main{text-align:center;padding:24px}
  .modes{display:inline-flex;gap:4px;background:#fff;border-radius:99px;padding:4px;margin-bottom:22px;box-shadow:0 1px 2px rgba(0,0,0,.05)}
  .modes button{border:0;background:none;border-radius:99px;padding:8px 16px;font:600 14px system-ui,sans-serif;color:var(--muted);cursor:pointer}
  .modes button[aria-pressed="true"]{background:var(--red);color:#fff}
  body.pause .modes button[aria-pressed="true"]{background:var(--green)}
  .dial{position:relative;width:240px;height:240px;margin:0 auto 22px}
  .dial svg{width:100%;height:100%;transform:rotate(-90deg)}
  .dial circle{fill:none;stroke-width:10}
  .track{stroke:#fff}
  .prog{stroke:var(--red);stroke-linecap:round;transition:stroke-dashoffset 1s linear}
  body.pause .prog{stroke:var(--green)}
  .time{position:absolute;inset:0;display:grid;place-content:center}
  .time b{font-size:54px;letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1}
  .time small{color:var(--muted);font-size:14px;margin-top:6px}
  .actions{display:flex;gap:10px;justify-content:center}
  .actions button{border:0;border-radius:14px;padding:13px 22px;font:700 15px system-ui,sans-serif;cursor:pointer}
  #go{background:var(--ink);color:#fff;min-width:130px}
  #reset{background:#fff;color:var(--ink)}
  .count{margin-top:22px;color:var(--muted);font-size:14px}
  .dots{display:flex;gap:6px;justify-content:center;margin-top:8px}
  .dots i{width:10px;height:10px;border-radius:50%;background:#fff;border:2px solid var(--red)}
  .dots i.on{background:var(--red)}
</style>
</head>
<body>
<main>
  <div class="modes" role="group" aria-label="Mode">
    <button type="button" data-mode="focus" aria-pressed="true">Concentration</button>
    <button type="button" data-mode="pause" aria-pressed="false">Pause</button>
  </div>
  <div class="dial">
    <svg viewBox="0 0 120 120" aria-hidden="true"><circle class="track" cx="60" cy="60" r="52"/><circle class="prog" id="prog" cx="60" cy="60" r="52"/></svg>
    <div class="time"><b id="clock" role="timer">25:00</b><small id="label">Séance de travail</small></div>
  </div>
  <div class="actions">
    <button type="button" id="go">Démarrer</button>
    <button type="button" id="reset">Réinitialiser</button>
  </div>
  <p class="count" id="count">0 séance terminée aujourd’hui</p>
  <div class="dots" id="dots" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
</main>

<script>
  const modes = { focus: { min: 25, label: 'Séance de travail' }, pause: { min: 5, label: 'Petite pause' } };
  const ring = 2 * Math.PI * 52;
  let mode = 'focus', left = modes.focus.min * 60, timer = null, done = 0;
  const $ = id => document.getElementById(id);
  $('prog').style.strokeDasharray = ring;

  function draw() {
    const total = modes[mode].min * 60;
    $('clock').textContent = String(Math.floor(left / 60)).padStart(2, '0') + ':' + String(left % 60).padStart(2, '0');
    $('prog').style.strokeDashoffset = ring * (1 - left / total);
    $('label').textContent = modes[mode].label;
    document.body.classList.toggle('pause', mode === 'pause');
    document.title = $('clock').textContent + ' · Focus';
  }

  function setMode(next) {
    mode = next;
    stop();
    left = modes[mode].min * 60;
    document.querySelectorAll('[data-mode]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.mode === mode)));
    draw();
  }

  function stop() {
    clearInterval(timer);
    timer = null;
    $('go').textContent = 'Démarrer';
  }

  function beep() {
    try {
      const ctx = new AudioContext();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.frequency.value = 880;
      gain.gain.setValueAtTime(0.2, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8);
      osc.connect(gain).connect(ctx.destination);
      osc.start();
      osc.stop(ctx.currentTime + 0.8);
    } catch (e) { /* son indisponible */ }
  }

  $('go').addEventListener('click', () => {
    if (timer) { stop(); return; }
    $('go').textContent = 'Pause';
    timer = setInterval(() => {
      left--;
      if (left <= 0) {
        beep();
        if (mode === 'focus') {
          done++;
          $('count').textContent = done + (done > 1 ? ' séances terminées' : ' séance terminée') + ' aujourd’hui';
          document.querySelectorAll('.dots i').forEach((dot, i) => dot.classList.toggle('on', i < done % 4 || (done % 4 === 0 && done > 0)));
        }
        setMode(mode === 'focus' ? 'pause' : 'focus');
        return;
      }
      draw();
    }, 1000);
  });
  $('reset').addEventListener('click', () => setMode(mode));
  document.querySelectorAll('[data-mode]').forEach(b => b.addEventListener('click', () => setMode(b.dataset.mode)));
  draw();
</script>
</body>
</html>

Ferramenta

Focus

O pedido

“Um cronômetro de foco: 25 minutos de trabalho, 5 minutos de pausa e um contador de sessões.”

O que funciona

  • Anel de progresso animado.
  • Passagem automática para a pausa.
  • Sinal sonoro no fim de cada sessão.