refactor(ui): simplify navigation and page layouts

- Replace dropdown hamburger menu with inline icon buttons for add,
  theme toggle, account/login
- Move sign-out button to account page header
- Remove unused icons (home, hamburger, close, checkin, timeline,
  squares) and add plus icon
- Simplify data page header (remove inline add button, now in nav)
This commit is contained in:
Jon Seager 2026-02-07 08:28:34 +00:00
parent 28a64a93d1
commit 47feedb273
No known key found for this signature in database
5 changed files with 87 additions and 99 deletions

View file

@ -221,7 +221,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<h2 class="text-lg font-semibold text-text">Data</h2> <h2 class="text-lg font-semibold text-text">Data</h2>
<p class="mt-1 text-sm text-text-secondary">Export all coffee data as JSON, or restore from a previous backup.</p> <p class="mt-1 text-sm text-text-secondary">Export all coffee data as JSON, restore from a previous backup, or reset to start fresh.</p>
</div> </div>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
@ -239,6 +239,13 @@
> >
Restore from Backup Restore from Backup
</button> </button>
<button
type="button"
class="rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-red-50 hover:border-red-300 dark:text-red-400 dark:hover:bg-red-950/30 dark:hover:border-red-800"
onclick="resetDatabase()"
>
Reset Database
</button>
</div> </div>
<input type="file" id="restore-file-input" accept=".json" class="hidden" <input type="file" id="restore-file-input" accept=".json" class="hidden"
@ -273,6 +280,24 @@
</section> </section>
{% endif %} {% endif %}
<!-- Sign Out -->
<section class="rounded-lg border bg-surface p-5">
<div class="flex flex-col gap-4">
<div>
<h2 class="text-lg font-semibold text-text">Sign Out</h2>
<p class="mt-1 text-sm text-text-secondary">Sign out of your account on this device.</p>
</div>
<div>
<form method="post" action="/logout">
<button type="submit" class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold text-text transition hover:border-accent hover:text-text">
{{ icons::logout("h-4 w-4") }}
Sign Out
</button>
</form>
</div>
</div>
</section>
<script> <script>
// --- Passkey form --- // --- Passkey form ---
@ -362,6 +387,35 @@
} }
}; };
const resetDatabase = async () => {
if (!confirm("Reset all coffee data?\n\nThis will permanently delete all roasters, roasts, bags, gear, brews, cafes, cups, and timeline events.\n\nThis cannot be undone.")) {
return;
}
if (!confirm("Are you sure? All coffee data will be permanently deleted.")) {
return;
}
const status = document.getElementById("backup-status");
const error = document.getElementById("backup-error");
status.classList.add("hidden");
error.classList.add("hidden");
try {
const response = await fetch("/api/v1/backup/reset", { method: "POST" });
if (!response.ok) {
throw new Error(`Reset failed (HTTP ${response.status}).`);
}
status.textContent = "Database reset successfully. All coffee data has been deleted.";
status.classList.remove("hidden");
} catch (err) {
error.textContent = err.message;
error.classList.remove("hidden");
}
};
const deletePasskey = async (id, name) => { const deletePasskey = async (id, name) => {
if (!confirm(`Delete passkey "${name}"? This cannot be undone.`)) return; if (!confirm(`Delete passkey "${name}"? This cannot be undone.`)) return;

View file

@ -1,19 +1,8 @@
{% extends "base.html" %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog · Data{% endblock %} {% extends "base.html" %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog · Data{% endblock %}
{% block content %} {% block content %}
<header class="flex flex-wrap items-start justify-between gap-4"> <header class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-semibold">Data</h1> <h1 class="text-3xl font-semibold">Data</h1>
<p class="max-w-2xl text-sm text-text-secondary">Browse all your coffee data.</p> <p class="max-w-2xl text-sm text-text-secondary">Browse all your coffee data.</p>
</div>
{% if is_authenticated %}
<a
href="/add?type={{ active_type }}"
class="inline-flex items-center gap-2 rounded-md bg-accent px-4 py-2.5 text-sm font-semibold text-accent-text transition hover:bg-accent-hover"
>
{{ icons::plus_circle("h-4 w-4") }}
Add
</a>
{% endif %}
</header> </header>
<div class="rounded-lg border bg-surface"> <div class="rounded-lg border bg-surface">

View file

@ -1,3 +1,9 @@
{% macro plus(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M11.25 3.75a1.25 1.25 0 1 0-2.5 0v4.5h-4.5a1.25 1.25 0 1 0 0 2.5h4.5v4.5a1.25 1.25 0 1 0 2.5 0v-4.5h4.5a1.25 1.25 0 1 0 0-2.5h-4.5v-4.5Z" />
</svg>
{% endmacro %}
{% macro camera(class) %} {% macro camera(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M1 8a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 018.07 3h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0016.07 6H17a2 2 0 012 2v7a2 2 0 01-2 2H3a2 2 0 01-2-2V8zm13.5 3a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM10 14a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M1 8a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 018.07 3h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0016.07 6H17a2 2 0 012 2v7a2 2 0 01-2 2H3a2 2 0 01-2-2V8zm13.5 3a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM10 14a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
@ -42,12 +48,6 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro home(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
{% endmacro %}
{% macro bag(class) %} {% macro bag(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M6 5v1H4.667a1.75 1.75 0 0 0-1.743 1.598l-.826 9.5A1.75 1.75 0 0 0 3.84 19H16.16a1.75 1.75 0 0 0 1.743-1.902l-.826-9.5A1.75 1.75 0 0 0 15.333 6H14V5a4 4 0 0 0-8 0Zm4-2.5A2.5 2.5 0 0 0 7.5 5v1h5V5A2.5 2.5 0 0 0 10 2.5ZM7.5 10a2.5 2.5 0 0 0 5 0V8.75a.75.75 0 0 1 1.5 0V10a4 4 0 0 1-8 0V8.75a.75.75 0 0 1 1.5 0V10Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M6 5v1H4.667a1.75 1.75 0 0 0-1.743 1.598l-.826 9.5A1.75 1.75 0 0 0 3.84 19H16.16a1.75 1.75 0 0 0 1.743-1.902l-.826-9.5A1.75 1.75 0 0 0 15.333 6H14V5a4 4 0 0 0-8 0Zm4-2.5A2.5 2.5 0 0 0 7.5 5v1h5V5A2.5 2.5 0 0 0 10 2.5ZM7.5 10a2.5 2.5 0 0 0 5 0V8.75a.75.75 0 0 1 1.5 0V10a4 4 0 0 1-8 0V8.75a.75.75 0 0 1 1.5 0V10Z" clip-rule="evenodd" />
@ -60,18 +60,6 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro hamburger(class) %}
<svg class="{{ class }}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
{% endmacro %}
{% macro close(class) %}
<svg class="{{ class }}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{% endmacro %}
{% macro arrow_up(class) %} {% macro arrow_up(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" />
@ -84,12 +72,6 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro checkin(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M9.69 18.933l.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 00.281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 103 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 002.273 1.765 11.842 11.842 0 00.976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro logout(class) %} {% macro logout(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M3 4.25A2.25 2.25 0 0 1 5.25 2h5.5A2.25 2.25 0 0 1 13 4.25v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 0-.75-.75h-5.5a.75.75 0 0 0-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75v-2a.75.75 0 0 1 1.5 0v2A2.25 2.25 0 0 1 10.75 18h-5.5A2.25 2.25 0 0 1 3 15.75V4.25Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M3 4.25A2.25 2.25 0 0 1 5.25 2h5.5A2.25 2.25 0 0 1 13 4.25v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 0-.75-.75h-5.5a.75.75 0 0 0-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75v-2a.75.75 0 0 1 1.5 0v2A2.25 2.25 0 0 1 10.75 18h-5.5A2.25 2.25 0 0 1 3 15.75V4.25Z" clip-rule="evenodd" />
@ -164,30 +146,6 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro timeline(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<rect x="3.25" y="1" width="1.5" height="18" rx="0.75"/>
<circle cx="4" cy="4" r="2"/>
<circle cx="4" cy="10" r="2"/>
<circle cx="4" cy="16" r="2"/>
<rect x="9" y="3" width="8" height="2" rx="1"/>
<rect x="9" y="9" width="6" height="2" rx="1"/>
<rect x="9" y="15" width="8" height="2" rx="1"/>
</svg>
{% endmacro %}
{% macro squares(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 0 0 2 4.25v2.5A2.25 2.25 0 0 0 4.25 9h2.5A2.25 2.25 0 0 0 9 6.75v-2.5A2.25 2.25 0 0 0 6.75 2h-2.5Zm0 9A2.25 2.25 0 0 0 2 13.25v2.5A2.25 2.25 0 0 0 4.25 18h2.5A2.25 2.25 0 0 0 9 15.75v-2.5A2.25 2.25 0 0 0 6.75 11h-2.5Zm9-9A2.25 2.25 0 0 0 11 4.25v2.5A2.25 2.25 0 0 0 13.25 9h2.5A2.25 2.25 0 0 0 18 6.75v-2.5A2.25 2.25 0 0 0 15.75 2h-2.5Zm0 9A2.25 2.25 0 0 0 11 13.25v2.5A2.25 2.25 0 0 0 13.25 18h2.5A2.25 2.25 0 0 0 18 15.75v-2.5A2.25 2.25 0 0 0 15.75 11h-2.5Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro cog(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.84 1.804A1 1 0 0 1 8.82 1h2.36a1 1 0 0 1 .98.804l.331 1.652a6.993 6.993 0 0 1 1.929 1.115l1.598-.54a1 1 0 0 1 1.186.447l1.18 2.044a1 1 0 0 1-.205 1.251l-1.267 1.113a7.047 7.047 0 0 1 0 2.228l1.267 1.113a1 1 0 0 1 .206 1.25l-1.18 2.045a1 1 0 0 1-1.187.447l-1.598-.54a6.993 6.993 0 0 1-1.929 1.115l-.33 1.652a1 1 0 0 1-.98.804H8.82a1 1 0 0 1-.98-.804l-.331-1.652a6.993 6.993 0 0 1-1.929-1.115l-1.598.54a1 1 0 0 1-1.186-.447l-1.18-2.044a1 1 0 0 1 .205-1.251l1.267-1.114a7.05 7.05 0 0 1 0-2.227L1.821 7.773a1 1 0 0 1-.206-1.25l1.18-2.045a1 1 0 0 1 1.187-.447l1.598.54A6.993 6.993 0 0 1 7.51 3.456l.33-1.652ZM10 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro check_circle(class) %} {% macro check_circle(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />

View file

@ -1,44 +1,24 @@
{% import "partials/icons.html" as icons %} {% import "partials/icons.html" as icons %}
<nav class="relative border-b pb-4 text-sm" data-signals:_nav-open="false"> <nav class="border-b pb-4 text-sm">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="font-semibold uppercase tracking-[0.25em] text-accent"><a href="/">B{rew}log</a></div> <div class="font-semibold uppercase tracking-[0.25em] text-accent"><a href="/">B{rew}log</a></div>
<div class="relative flex items-center gap-1"> <div class="flex items-center gap-1">
<a class="rounded-md p-1.5 transition {% if nav_active == "home" %}text-accent{% else %}text-text-muted hover:text-text-secondary{% endif %}" href="/" title="Home" aria-label="Home"> {% if is_authenticated %}
{{ icons::home("h-5 w-5") }} <a class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" href="/add" title="Add" aria-label="Add">
{{ icons::plus("h-5 w-5") }}
</a> </a>
{% endif %}
<button type="button" class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" title="Toggle theme" aria-label="Toggle theme" onclick="toggleTheme()"> <button type="button" class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" title="Toggle theme" aria-label="Toggle theme" onclick="toggleTheme()">
<span class="theme-icon-light">{{ icons::sun("h-5 w-5") }}</span> <span class="theme-icon-light">{{ icons::sun("h-5 w-5") }}</span>
<span class="theme-icon-dark hidden">{{ icons::moon("h-5 w-5") }}</span> <span class="theme-icon-dark hidden">{{ icons::moon("h-5 w-5") }}</span>
</button> </button>
<button class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" data-on:click="$_navOpen = !$_navOpen" title="Menu" aria-label="Toggle menu">
<span data-show="!$_navOpen">{{ icons::hamburger("h-5 w-5") }}</span>
<span data-show="$_navOpen" style="display:none">{{ icons::close("h-5 w-5") }}</span>
</button>
<!-- Backdrop (click outside to close) -->
<div class="fixed inset-0 z-40" data-show="$_navOpen" data-on:click="$_navOpen = false" style="display:none"></div>
<!-- Dropdown menu -->
<div class="absolute right-0 top-full z-50 mt-2 flex flex-col gap-1 rounded-lg border bg-surface p-3 whitespace-nowrap" data-show="$_navOpen" style="display:none">
<a class="rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 {% if nav_active == "data" %}text-accent font-medium{% else %}text-text-muted hover:text-text hover:bg-surface-alt{% endif %}" href="/data">
Data {{ icons::squares("h-4 w-4 shrink-0") }}
</a>
<a class="rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 {% if nav_active == "timeline" %}text-accent font-medium{% else %}text-text-muted hover:text-text hover:bg-surface-alt{% endif %}" href="/timeline">
Timeline {{ icons::timeline("h-4 w-4 shrink-0") }}
</a>
{% if is_authenticated %} {% if is_authenticated %}
<a class="rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 {% if nav_active == "checkin" %}text-accent font-medium{% else %}text-text-muted hover:text-text hover:bg-surface-alt{% endif %}" href="/check-in"> <a class="rounded-md p-1.5 transition {% if nav_active == "account" %}text-accent{% else %}text-text-muted hover:text-text-secondary{% endif %}" href="/account" title="Account" aria-label="Account">
Check In {{ icons::checkin("h-4 w-4 shrink-0") }} {{ icons::user("h-5 w-5") }}
</a> </a>
<a class="rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 {% if nav_active == "account" %}text-accent font-medium{% else %}text-text-muted hover:text-text hover:bg-surface-alt{% endif %}" href="/account">
Admin {{ icons::cog("h-4 w-4 shrink-0") }}
</a>
<form method="post" action="/logout">
<button type="submit" class="w-full rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 text-text-muted hover:text-text hover:bg-surface-alt">
Sign Out {{ icons::logout("h-4 w-4 shrink-0") }}
</button>
</form>
{% else %} {% else %}
<a class="rounded-md px-4 py-2.5 transition flex items-center justify-end gap-4 text-text-muted hover:text-text hover:bg-surface-alt" href="/login"> <a class="rounded-md p-1.5 text-text-muted transition hover:text-text-secondary" href="/login" title="Login" aria-label="Login">
Login {{ icons::user("h-4 w-4 shrink-0") }} {{ icons::user("h-5 w-5") }}
</a> </a>
{% endif %} {% endif %}
</div> </div>

View file

@ -9,6 +9,13 @@
> >
{{ icons::camera("h-5 w-5") }} {{ icons::camera("h-5 w-5") }}
</brew-photo-capture> </brew-photo-capture>
<a href="/check-in"
class="shrink-0 inline-flex items-center justify-center rounded-md border p-2.5 text-accent transition hover:bg-surface-alt"
title="Check in at a cafe"
aria-label="Check in at a cafe"
>
{{ icons::location("h-5 w-5") }}
</a>
<input <input
type="text" type="text"
name="prompt" name="prompt"