index.html

por Felipe Augusto Barcelos última modificação 07/05/2026 14h52

HTML icon index.html — HTML, 11 KB (12265 bytes)

Conteúdo do arquivo

<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Carta de Serviço 2026</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #eef2f6;
      --paper: #ffffff;
      --ink: #172033;
      --muted: #647086;
      --line: #cfd7e3;
      --accent: #0b6f6a;
      --accent-strong: #07524f;
      --shadow: 0 18px 45px rgba(23, 32, 51, .22);
      font-family: Arial, Helvetica, sans-serif;
    }

    * { box-sizing: border-box; }

    html,
    body {
      width: 100%;
      min-height: 100%;
      margin: 0;
      background: var(--bg);
      color: var(--ink);
    }

    body {
      overflow: hidden;
    }

    .app {
      display: grid;
      grid-template-rows: auto 1fr;
      min-height: 100vh;
    }

    .toolbar {
      display: flex;
      align-items: center;
      gap: 8px;
      min-height: 58px;
      padding: 10px 12px;
      border-bottom: 1px solid var(--line);
      background: rgba(255, 255, 255, .96);
      box-shadow: 0 1px 0 rgba(23, 32, 51, .04);
      z-index: 5;
    }

    .title {
      min-width: 155px;
      margin-right: auto;
      line-height: 1.15;
    }

    .title strong {
      display: block;
      font-size: 15px;
      font-weight: 700;
    }

    .title span {
      display: block;
      color: var(--muted);
      font-size: 12px;
      margin-top: 2px;
    }

    button,
    a.button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 40px;
      height: 38px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: #fff;
      color: var(--ink);
      font: inherit;
      line-height: 1;
      text-decoration: none;
      cursor: pointer;
      transition: background .16s ease, border-color .16s ease, transform .16s ease;
    }

    button:hover,
    a.button:hover {
      background: #f7fafc;
      border-color: #aeb9c8;
    }

    button:active,
    a.button:active {
      transform: translateY(1px);
    }

    button:disabled {
      cursor: not-allowed;
      opacity: .45;
    }

    .counter {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 86px;
      height: 38px;
      padding: 0 10px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: #fff;
      color: var(--muted);
      font-size: 13px;
      white-space: nowrap;
    }

    .stage {
      position: relative;
      display: grid;
      place-items: center;
      min-height: 0;
      padding: 18px;
      overflow: auto;
      background:
        linear-gradient(90deg, rgba(11, 111, 106, .08), transparent 28%, transparent 72%, rgba(11, 111, 106, .08)),
        var(--bg);
    }

    .book {
      position: relative;
      width: min(92vw, 980px);
      aspect-ratio: 1.414 / 1;
      perspective: 1800px;
    }

    .spread {
      position: absolute;
      inset: 0;
      display: grid;
      grid-template-columns: 1fr 1fr;
      filter: drop-shadow(var(--shadow));
      transform-origin: center center;
      transition: transform .2s ease;
    }

    .sheet {
      position: relative;
      overflow: hidden;
      background: var(--paper);
    }

    .sheet.left {
      border-radius: 6px 0 0 6px;
      box-shadow: inset -10px 0 16px rgba(23, 32, 51, .10);
    }

    .sheet.right {
      border-radius: 0 6px 6px 0;
      box-shadow: inset 10px 0 16px rgba(23, 32, 51, .08);
    }

    .sheet.single {
      grid-column: 1 / -1;
      justify-self: center;
      width: 50%;
      border-radius: 6px;
      box-shadow: none;
    }

    .page {
      width: 100%;
      height: 100%;
      object-fit: contain;
      display: block;
      background: #fff;
      user-select: none;
      -webkit-user-drag: none;
    }

    .turning .right {
      animation: turnPage .46s ease;
      transform-origin: left center;
    }

    .turning-back .left {
      animation: turnBack .46s ease;
      transform-origin: right center;
    }

    @keyframes turnPage {
      0% { transform: rotateY(0); filter: brightness(1); }
      50% { transform: rotateY(-52deg); filter: brightness(.92); }
      100% { transform: rotateY(0); filter: brightness(1); }
    }

    @keyframes turnBack {
      0% { transform: rotateY(0); filter: brightness(1); }
      50% { transform: rotateY(52deg); filter: brightness(.92); }
      100% { transform: rotateY(0); filter: brightness(1); }
    }

    .edge {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 22%;
      border: 0;
      background: transparent;
      z-index: 3;
    }

    .edge.prev { left: 0; }
    .edge.next { right: 0; }

    .edge:focus-visible {
      outline: 3px solid rgba(11, 111, 106, .45);
      outline-offset: 3px;
    }

    @media (max-width: 760px) {
      body { overflow: auto; }

      .app { min-height: 100svh; }

      .toolbar {
        flex-wrap: wrap;
        align-content: center;
      }

      .title {
        flex: 1 1 100%;
        min-width: 0;
        margin-right: 0;
      }

      .stage {
        padding: 12px;
        align-items: start;
      }

      .book {
        width: min(94vw, 520px);
        aspect-ratio: .707 / 1;
      }

      .spread {
        grid-template-columns: 1fr;
      }

      .sheet,
      .sheet.single {
        grid-column: auto;
        width: 100%;
        border-radius: 6px;
        box-shadow: none;
      }

      .sheet.left { display: none; }
    }
  </style>
</head>
<body>
  <main class="app">
    <nav class="toolbar" aria-label="Controles do flipbook">
      <div class="title">
        <strong>Carta de Serviço 2026</strong>
        <span>Câmara Municipal</span>
      </div>
      <button id="first" type="button" title="Primeira página" aria-label="Primeira página">|&lt;</button>
      <button id="prev" type="button" title="Página anterior" aria-label="Página anterior">&lt;</button>
      <span class="counter" id="counter">1 / 16</span>
      <button id="next" type="button" title="Próxima página" aria-label="Próxima página">&gt;</button>
      <button id="last" type="button" title="Última página" aria-label="Última página">&gt;|</button>
      <button id="zoomOut" type="button" title="Diminuir zoom" aria-label="Diminuir zoom">-</button>
      <button id="zoomIn" type="button" title="Aumentar zoom" aria-label="Aumentar zoom">+</button>
      <button id="fullscreen" type="button" title="Tela cheia" aria-label="Tela cheia">[]</button>
      <a class="button" href="carta-de-servico-2026.pdf" target="_blank" rel="noopener" title="Abrir PDF" aria-label="Abrir PDF">PDF</a>
    </nav>
    <section class="stage" id="stage">
      <div class="book" id="book">
        <div class="spread" id="spread"></div>
        <button class="edge prev" id="edgePrev" type="button" aria-label="Página anterior"></button>
        <button class="edge next" id="edgeNext" type="button" aria-label="Próxima página"></button>
      </div>
    </section>
  </main>
  <script>
    const pages = [{"src":"pages/page-01.png","alt":"Carta de Serviço 2026 - página 1"},{"src":"pages/page-02.png","alt":"Carta de Serviço 2026 - página 2"},{"src":"pages/page-03.png","alt":"Carta de Serviço 2026 - página 3"},{"src":"pages/page-04.png","alt":"Carta de Serviço 2026 - página 4"},{"src":"pages/page-05.png","alt":"Carta de Serviço 2026 - página 5"},{"src":"pages/page-06.png","alt":"Carta de Serviço 2026 - página 6"},{"src":"pages/page-07.png","alt":"Carta de Serviço 2026 - página 7"},{"src":"pages/page-08.png","alt":"Carta de Serviço 2026 - página 8"},{"src":"pages/page-09.png","alt":"Carta de Serviço 2026 - página 9"},{"src":"pages/page-10.png","alt":"Carta de Serviço 2026 - página 10"},{"src":"pages/page-11.png","alt":"Carta de Serviço 2026 - página 11"},{"src":"pages/page-12.png","alt":"Carta de Serviço 2026 - página 12"},{"src":"pages/page-13.png","alt":"Carta de Serviço 2026 - página 13"},{"src":"pages/page-14.png","alt":"Carta de Serviço 2026 - página 14"},{"src":"pages/page-15.png","alt":"Carta de Serviço 2026 - página 15"},{"src":"pages/page-16.png","alt":"Carta de Serviço 2026 - página 16"}];
    const spread = document.getElementById("spread");
    const book = document.getElementById("book");
    const counter = document.getElementById("counter");
    const stage = document.getElementById("stage");
    const controls = {
      first: document.getElementById("first"),
      prev: document.getElementById("prev"),
      next: document.getElementById("next"),
      last: document.getElementById("last"),
      edgePrev: document.getElementById("edgePrev"),
      edgeNext: document.getElementById("edgeNext"),
      zoomOut: document.getElementById("zoomOut"),
      zoomIn: document.getElementById("zoomIn"),
      fullscreen: document.getElementById("fullscreen"),
    };
    let index = 0;
    let zoom = 1;

    function isMobileLayout() {
      return window.matchMedia("(max-width: 760px)").matches;
    }

    function pageNode(page, side) {
      const sheet = document.createElement("div");
      sheet.className = "sheet " + side;
      const img = document.createElement("img");
      img.className = "page";
      img.src = page.src;
      img.alt = page.alt;
      img.loading = "eager";
      sheet.appendChild(img);
      return sheet;
    }

    function render(direction = 0) {
      spread.classList.remove("turning", "turning-back");
      void spread.offsetWidth;
      if (direction > 0) spread.classList.add("turning");
      if (direction < 0) spread.classList.add("turning-back");

      spread.innerHTML = "";
      const mobile = isMobileLayout();
      if (mobile || index === 0 || index === pages.length - 1) {
        spread.appendChild(pageNode(pages[index], "single right"));
      } else {
        const leftIndex = index % 2 === 0 ? index - 1 : index;
        const rightIndex = leftIndex + 1;
        spread.appendChild(pageNode(pages[leftIndex], "left"));
        if (pages[rightIndex]) spread.appendChild(pageNode(pages[rightIndex], "right"));
      }

      const shown = mobile || index === 0 || index === pages.length - 1
        ? String(index + 1)
        : String((index % 2 === 0 ? index : index + 1)) + "-" + String(Math.min((index % 2 === 0 ? index + 1 : index + 2), pages.length));
      counter.textContent = shown + " / " + pages.length;
      controls.first.disabled = index === 0;
      controls.prev.disabled = index === 0;
      controls.next.disabled = index >= pages.length - 1;
      controls.last.disabled = index >= pages.length - 1;
      spread.style.transform = "scale(" + zoom.toFixed(2) + ")";
    }

    function goTo(nextIndex, direction) {
      const bounded = Math.max(0, Math.min(pages.length - 1, nextIndex));
      if (bounded === index) return;
      index = bounded;
      render(direction);
    }

    function nextPage() {
      goTo(isMobileLayout() || index === 0 ? index + 1 : index + 2, 1);
    }

    function prevPage() {
      goTo(isMobileLayout() || index <= 1 ? index - 1 : index - 2, -1);
    }

    controls.first.addEventListener("click", () => goTo(0, -1));
    controls.prev.addEventListener("click", prevPage);
    controls.edgePrev.addEventListener("click", prevPage);
    controls.next.addEventListener("click", nextPage);
    controls.edgeNext.addEventListener("click", nextPage);
    controls.last.addEventListener("click", () => goTo(pages.length - 1, 1));
    controls.zoomOut.addEventListener("click", () => { zoom = Math.max(.75, zoom - .1); render(); });
    controls.zoomIn.addEventListener("click", () => { zoom = Math.min(1.8, zoom + .1); render(); });
    controls.fullscreen.addEventListener("click", () => {
      if (!document.fullscreenElement) stage.requestFullscreen?.();
      else document.exitFullscreen?.();
    });

    window.addEventListener("keydown", event => {
      if (event.key === "ArrowRight" || event.key === "PageDown") nextPage();
      if (event.key === "ArrowLeft" || event.key === "PageUp") prevPage();
      if (event.key === "Home") goTo(0, -1);
      if (event.key === "End") goTo(pages.length - 1, 1);
    });
    window.addEventListener("resize", () => render());
    render();
  </script>
</body>
</html>