Zum Inhalt springen
nella

Kreationen

Beispiele, die funktionieren.

Jedes Beispiel ist eine echte Seite aus HTML, CSS und JavaScript. Sie läuft hier, in einem isolierten Rahmen, genau wie eine Kreation von Nella. Klicken, tippen, spielen Sie.

Diese Beispiele wurden für diese Seite erstellt, um zu zeigen, welche Ergebnisse möglich sind. Sie sind fiktiv und auf Französisch verfasst.

Trinkgeldrechner Beispiel auf Französisch
Code Vorschau
<!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>

App

Trinkgeldrechner

Die Anfrage

„Ein Trinkgeldrechner für Québec: Bundes- und Provinzsteuer, frei wählbarer Prozentsatz und Aufteilung auf mehrere Personen.“

Was funktioniert

  • Steuern von 5 % und 9,975 %, auf den Cent gerundet.
  • Trinkgeld vor oder nach Steuern berechnet.
  • Aufteilung der Rechnung, auf den Cent aufgerundet.
Mie & Levain Beispiel auf Französisch
Code Vorschau
<!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>

Firmen-Website

Mie & Levain

Die Anfrage

„Die Website meiner Bäckerei: unsere Brote, unsere Preise, die Öffnungszeiten und Reservierungen für den nächsten Tag.“

Was funktioniert

  • Produktfilter.
  • Reservierung mit Gesamtbetrag in Dollar.
  • Hinweis „Jetzt geöffnet“ nach der tatsächlichen Uhrzeit.
Paires Beispiel auf Französisch
Code Vorschau
<!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>

Spiel

Paires

Die Anfrage

„Ein Memory-Spiel mit bunten Formen, einer Stoppuhr und einem Zugzähler.“

Was funktioniert

  • Karten werden jede Runde neu gemischt.
  • Stoppuhr und Zugzähler.
  • Mit der Tastatur spielbar.
Budget du mois Beispiel auf Französisch
Code Vorschau
<!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>

Tabelle

Budget du mois

Die Anfrage

„Eine bearbeitbare Monatsbudget-Tabelle mit dem verbleibenden Betrag und einem Diagramm nach Kategorie.“

Was funktioniert

  • Bearbeitbare Zeilen, hinzufügen und löschen.
  • Summe, Restbetrag und Anteil am Einkommen.
  • Diagramm bei jeder Eingabe neu berechnet.
Orbite Beispiel auf Französisch
Code Vorschau
<!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>

Launch-Seite

Orbite

Die Anfrage

„Eine Launch-Seite für eine Kalender-App, mit Countdown und Warteliste.“

Was funktioniert

  • Live-Countdown.
  • Prüfung der E-Mail-Adresse.
  • Dunkles Layout, für Mobilgeräte optimiert.
Focus Beispiel auf Französisch
Code Vorschau
<!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>

Werkzeug

Focus

Die Anfrage

„Ein Fokus-Timer: 25 Minuten Arbeit, 5 Minuten Pause und ein Zähler für die Einheiten.“

Was funktioniert

  • Animierter Fortschrittsring.
  • Automatischer Wechsel in die Pause.
  • Signalton am Ende jeder Einheit.