| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- {% extends "layout.twig" %}
- {% block content %}
- <section class="space-y-12" x-data="screenshotModal">
- {# --------------- Hero --------------- #}
- <header class="text-center max-w-3xl mx-auto pt-2">
- <div class="flex justify-center mb-4">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="96" height="96" aria-hidden="true" class="block" fill="none">
- <defs>
- <radialGradient id="brand-cycle-glow-about" cx="32" cy="32" r="20" gradientUnits="userSpaceOnUse">
- <stop offset="0" stop-color="#6366f1" stop-opacity="0.55"/>
- <stop offset="0.6" stop-color="#6366f1" stop-opacity="0.12"/>
- <stop offset="1" stop-color="#6366f1" stop-opacity="0"/>
- </radialGradient>
- </defs>
- <circle cx="32" cy="32" r="20" fill="url(#brand-cycle-glow-about)"/>
- <path d="M52 32 A20 20 0 1 1 32 12" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"/>
- <path d="M44 8 L52 12 L48 20" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
- <circle cx="32" cy="32" r="5" fill="#6366f1"/>
- <circle cx="48" cy="20" r="2.5" fill="currentColor" opacity="0.55"/>
- <circle cx="52" cy="40" r="2.5" fill="currentColor" opacity="0.55"/>
- <circle cx="40" cy="50" r="2.5" fill="currentColor" opacity="0.55"/>
- <circle cx="20" cy="48" r="2.5" fill="currentColor" opacity="0.55"/>
- <circle cx="14" cy="32" r="2.5" fill="#10b981"/>
- </svg>
- </div>
- <h1 class="text-4xl font-bold tracking-tight">Sprint Planner</h1>
- <p class="mt-3 text-lg text-slate-600 dark:text-slate-300">
- One shared sprint board for the whole team — replacing the
- spreadsheet that everybody copies and nobody keeps in sync.
- </p>
- </header>
- {# --------------- What it is --------------- #}
- <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
- <h2 class="text-2xl font-semibold tracking-tight">What it is</h2>
- <p class="mt-3 text-slate-700 dark:text-slate-300">
- Sprint Planner is a small web app for ops/dev teams that plan their
- work in two- to four-week sprints. It replaces an Excel sprint
- workbook with a proper website: shared by everyone, edited live,
- backed by a database, and protected with sign-in. Every change is
- recorded so it is always clear who moved a number and when.
- </p>
- <p class="mt-3 text-slate-700 dark:text-slate-300">
- Each sprint has its own page. The top of the page is a
- <em>working-days matrix</em> — for every week of the sprint, every
- team member tells the planner how many days they will be available.
- The bottom is the <em>task list</em> — every piece of work the
- team will tackle, with the days each person is going to spend on
- it. The page sums these numbers up live so the team can see
- whether they are over- or under-committed for the sprint.
- </p>
- </div>
- {# --------------- How it works --------------- #}
- <div class="space-y-6">
- <h2 class="text-2xl font-semibold tracking-tight text-center">How a sprint runs end-to-end</h2>
- <ol class="grid gap-4 md:grid-cols-2">
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">1</span>
- <h3 class="mt-3 font-semibold">Create the sprint</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- An admin gives the sprint a name and picks a start and end
- date. The planner splits that range into calendar weeks
- automatically — up to twenty-six.
- </p>
- </li>
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">2</span>
- <h3 class="mt-3 font-semibold">Pick the team</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Add the people who will work on this sprint, set the
- reserve buffer (how much capacity to keep aside for
- interruptions), and tick the public holidays so the
- available-days count is right out of the box.
- </p>
- </li>
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">3</span>
- <h3 class="mt-3 font-semibold">Capture the work</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Add tasks with a title, an owner, a priority, and an
- estimate of total days. Re-order them by drag-and-drop;
- filter by owner, priority, or text search.
- </p>
- </li>
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">4</span>
- <h3 class="mt-3 font-semibold">Allocate days</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- For every task, type how many days each person will spend.
- The planner re-totals the row, deducts the reserve, and
- shows what each person still has free — in green when
- there is room, in red when overcommitted.
- </p>
- </li>
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">5</span>
- <h3 class="mt-3 font-semibold">Track progress</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Mark each cell as <em>started</em>, <em>done</em>, or
- <em>cancelled</em> as the sprint runs. Use the focus
- filter to look at one person's plate at a time without
- losing the rest.
- </p>
- </li>
- <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">6</span>
- <h3 class="mt-3 font-semibold">Present and audit</h3>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- A big-screen view trims the chrome down for the daily
- stand-up or the sprint review. The audit log captures
- every edit — who, when, before, after — so questions
- like “wait, who changed that number?” have a
- factual answer.
- </p>
- </li>
- </ol>
- </div>
- {# --------------- Tour of the interface (gallery) --------------- #}
- <div class="space-y-6">
- <h2 class="text-2xl font-semibold tracking-tight text-center">A tour of the interface</h2>
- <p class="text-center text-slate-600 dark:text-slate-400 max-w-2xl mx-auto text-sm">
- Click any picture to enlarge it.
- </p>
- <div class="grid gap-6 md:grid-cols-2">
- {# 1. Sprints overview #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprints_view.png"
- data-alt="Sprints overview"
- aria-label="Enlarge: Sprints overview"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Sprints_view.png"
- alt="Sprints overview"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Sprints overview</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprints_view.png"
- data-alt="Sprints overview"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- The home page lists every sprint with its dates, worker
- count, task count, and reserve percentage. Click a row
- to open it.
- </p>
- </figcaption>
- </figure>
- {# 2. Sprint settings #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_settings.png"
- data-alt="Sprint settings"
- aria-label="Enlarge: Sprint settings"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Sprint_settings.png"
- alt="Sprint settings"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Sprint settings</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_settings.png"
- data-alt="Sprint settings"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Edit the dates and reserve buffer, add or remove team
- members, mark which weekdays are working days for each
- week, and set a per-person reserve.
- </p>
- </figcaption>
- </figure>
- {# 3. Working-days matrix #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_recources_view.png"
- data-alt="Working-days matrix (Arbeitstage)"
- aria-label="Enlarge: Working-days matrix"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Sprint_recources_view.png"
- alt="Working-days matrix (Arbeitstage)"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Working-days matrix</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_recources_view.png"
- data-alt="Working-days matrix (Arbeitstage)"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- For every team member, type the number of days they
- will be available each week. Half-day steps are
- allowed. The summary row at the top shows the total
- capacity for the whole sprint.
- </p>
- </figcaption>
- </figure>
- {# 4. Task list #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_task_view.png"
- data-alt="Task list with per-worker allocations"
- aria-label="Enlarge: Task list"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Sprint_task_view.png"
- alt="Task list with per-worker allocations"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Task list</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_task_view.png"
- data-alt="Task list with per-worker allocations"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Every task is one row: title, owner, priority, total
- days, and one cell per team member. Search, sort,
- filter by owner or priority, hide the columns you
- don't need, and drag rows to reorder.
- </p>
- </figcaption>
- </figure>
- {# 5. Cell time + status picker #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Task_user_time_status.png"
- data-alt="Per-cell time and status picker"
- aria-label="Enlarge: Per-cell time and status picker"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Task_user_time_status.png"
- alt="Per-cell time and status picker"
- class="w-full h-56 object-contain transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Cell time & status</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Task_user_time_status.png"
- data-alt="Per-cell time and status picker"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Click a cell to slide the day count and tag progress
- — assigned, started, done, or cancelled. The cell tints
- yellow / green / red so the team can scan progress at
- a glance.
- </p>
- </figcaption>
- </figure>
- {# 6. Task menu #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Task_edit_menu.png"
- data-alt="Task menu — edit, move, copy, delete"
- aria-label="Enlarge: Task menu"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Task_edit_menu.png"
- alt="Task menu — edit, move, copy, delete"
- class="w-full h-56 object-contain transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Task menu</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Task_edit_menu.png"
- data-alt="Task menu — edit, move, copy, delete"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Each task carries a small menu: edit the title and
- description, attach a link, move or copy the task to
- another sprint, or delete it. The right-hand pane
- shows the read-only details.
- </p>
- </figcaption>
- </figure>
- {# 7. Presenter view #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_presenter_screen.png"
- data-alt="Big-screen presenter view"
- aria-label="Enlarge: Big-screen presenter view"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Sprint_presenter_screen.png"
- alt="Big-screen presenter view"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Big-screen presenter</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Sprint_presenter_screen.png"
- data-alt="Big-screen presenter view"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- A stripped-down view tuned for the projector at a
- stand-up. Worker columns can rotate to fit a wide
- team into one screen without scrolling.
- </p>
- </figcaption>
- </figure>
- {# 8. Audit log #}
- <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Auditlog.png"
- data-alt="Audit log"
- aria-label="Enlarge: Audit log"
- class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
- <img src="/assets/img/screenshots/Auditlog.png"
- alt="Audit log"
- class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
- </button>
- <figcaption class="p-4 flex-1 flex flex-col">
- <div class="flex items-baseline justify-between gap-3">
- <h3 class="font-semibold">Audit log</h3>
- <button type="button"
- x-on:click="open($event)"
- data-src="/assets/img/screenshots/Auditlog.png"
- data-alt="Audit log"
- class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
- </div>
- <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
- Every create, update, and delete is logged with user,
- time, and a before/after snapshot. Filter by user,
- action, entity, or date range — useful when something
- looks off and someone wants to know what happened.
- </p>
- </figcaption>
- </figure>
- </div>
- </div>
- {# --------------- Quickstart --------------- #}
- <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
- <h2 class="text-2xl font-semibold tracking-tight">Quickstart</h2>
- <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
- For a pilot install on one host. You need <strong>Docker</strong>
- (with the <code>docker compose</code> plugin) and not much else.
- </p>
- <ol class="mt-5 space-y-5 text-sm">
- <li>
- <p class="font-semibold">1. Clone the repository.</p>
- <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>git clone https://git.chiapparini.org/chiappa/sprint_planer_web.git
- cd sprint_planer_web</code></pre>
- </li>
- <li>
- <p class="font-semibold">2. Make a config file.</p>
- <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>cp .env.example .env
- chmod 600 .env</code></pre>
- <p class="mt-2 text-slate-600 dark:text-slate-400">
- Open <code>.env</code> and either fill in the
- <code>ENTRA_*</code> values (to sign in with a Microsoft
- work account) or set
- <code>LOCAL_ADMIN_EMAIL</code> and
- <code>LOCAL_ADMIN_PASSWORD_HASH</code> for a local sign-in.
- </p>
- </li>
- <li>
- <p class="font-semibold">3. Generate the local-admin password hash (only if you use the local fallback).</p>
- <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>docker run --rm -it php:8.3-cli php -r \
- 'echo password_hash(readline("Password: "), PASSWORD_DEFAULT), PHP_EOL;'</code></pre>
- <p class="mt-2 text-slate-600 dark:text-slate-400">
- Paste the resulting <code>$2y$…</code> string into
- <code>.env</code> as <code>LOCAL_ADMIN_PASSWORD_HASH</code>
- — keep the single quotes around it.
- </p>
- </li>
- <li>
- <p class="font-semibold">4. Build and start the stack.</p>
- <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>docker compose up -d --build</code></pre>
- <p class="mt-2 text-slate-600 dark:text-slate-400">
- The default port is <code>8080</code>; change
- <code>HTTP_PORT</code> in <code>.env</code> if you need
- something else.
- </p>
- </li>
- <li>
- <p class="font-semibold">5. Open the app and sign in.</p>
- <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>http://localhost:8080</code></pre>
- <p class="mt-2 text-slate-600 dark:text-slate-400">
- The first person to sign in via local admin (or whoever
- you nominated through <code>BOOTSTRAP_ADMIN_*</code> on
- the Microsoft path) becomes the administrator.
- Subsequent admin promotions happen on the
- <strong>Users</strong> page.
- </p>
- </li>
- </ol>
- <p class="mt-5 text-xs text-slate-500 dark:text-slate-400">
- Backups, upgrades, reverse-proxy setup, Microsoft Entra
- registration: see <code>doc/admin-manual.md</code> in the
- repository.
- </p>
- </div>
- {# --------------- Source / license / version --------------- #}
- <div class="rounded-lg border bg-slate-50 p-6 dark:bg-slate-800/60 dark:border-slate-700 text-sm">
- <div class="grid gap-6 md:grid-cols-3">
- <div>
- <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">Source</h3>
- <a href="https://git.chiapparini.org/chiappa/sprint_planer_web"
- class="mt-1 inline-block text-blue-700 hover:underline dark:text-blue-400 break-all"
- rel="noopener noreferrer">
- git.chiapparini.org/chiappa/sprint_planer_web
- </a>
- </div>
- <div>
- <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">License</h3>
- <p class="mt-1">
- <a href="https://www.apache.org/licenses/LICENSE-2.0"
- class="text-blue-700 hover:underline dark:text-blue-400"
- rel="noopener noreferrer">Apache License 2.0</a><br>
- <span class="text-slate-600 dark:text-slate-400">© 2026 {{ appCreator }}</span>
- </p>
- </div>
- <div>
- <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">Version</h3>
- <p class="mt-1 font-mono">{{ appVersion }}</p>
- </div>
- </div>
- </div>
- {# --------------- Modal overlay --------------- #}
- <div x-show="isOpen"
- x-on:keydown.escape.window="close()"
- x-cloak
- role="dialog"
- aria-modal="true"
- aria-label="Enlarged screenshot"
- class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-8 bg-slate-900/85 backdrop-blur-sm">
- {# Click-to-dismiss backdrop. Sits behind the image / close button so
- clicks on either don't propagate. #}
- <button type="button"
- x-on:click="close()"
- aria-label="Close enlarged screenshot"
- tabindex="-1"
- class="absolute inset-0 w-full h-full cursor-default focus:outline-none"></button>
- {# Prominent close button — top-right, white pill. #}
- <button type="button"
- x-on:click="close()"
- aria-label="Close"
- class="absolute top-4 right-4 z-10 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-semibold text-slate-900 shadow-lg hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-white">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
- <line x1="5" y1="5" x2="15" y2="15"></line>
- <line x1="15" y1="5" x2="5" y2="15"></line>
- </svg>
- <span>Close</span>
- </button>
- {# The enlarged image itself — `pointer-events-none` would block our
- backdrop button below from the image's footprint, so we leave it
- interactive but it has no click handler — clicking it does nothing. #}
- <img x-bind:src="src"
- x-bind:alt="alt"
- class="relative max-w-[95vw] max-h-[90vh] object-contain rounded shadow-2xl">
- </div>
- </section>
- {% endblock %}
|