brewlog/templates/data.html
Jon Seager fb67f0659c
feat(ui): redesign with theme tokens, dark mode, and reusable components
Overhaul the web UI with CSS custom property-based theming and
dark mode support. Extract reusable template partials (location
search, scan input) and web components (photo-capture,
searchable-select). Add version and commit info to page footer.

- Replace hardcoded amber palette with semantic theme tokens
- Add dark mode with localStorage persistence and system preference detection
- Extract <brew-photo-capture> and <searchable-select> web components
- Extract location_search and scan_input Askama macros
- Add sun/moon, timeline, database, map icons
- Display version and git commit in footer
- Improve timeline coordinate rounding and map link formatting
- Add roast_name, roaster_name, remaining to BagOptionView
2026-02-05 17:03:09 +00:00

108 lines
4.1 KiB
HTML

{% extends "base.html" %} {% import "partials/icons.html" as icons %} {% block title %}Brewlog · Data{% endblock %}
{% block content %}
<header class="flex flex-wrap items-start justify-between gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-semibold">Data</h1>
<p class="max-w-2xl text-sm text-stone-600">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"
>
{% call icons::plus_circle("h-4 w-4") %}
Add
</a>
{% endif %}
</header>
<div class="rounded-lg border bg-surface"
data-signals:_active-tab="'{{ active_type }}'" data-signals:_tabs-open="false">
<!-- Desktop tabs -->
<nav class="hidden md:flex flex-wrap gap-1 p-1.5">
{% for tab in tabs %}
<a
href="/data?type={{ tab.key }}"
data-class:bg-accent="$_activeTab === '{{ tab.key }}'"
data-class:text-white="$_activeTab === '{{ tab.key }}'"
data-class:shadow-sm="$_activeTab === '{{ tab.key }}'"
data-class:text-stone-600="$_activeTab !== '{{ tab.key }}'"
class="rounded-md px-3 py-1.5 text-sm font-medium transition"
data-on:click__prevent="$_activeTab = '{{ tab.key }}'; history.pushState(null, '', '/data?type={{ tab.key }}'); @get('/data?type={{ tab.key }}', {responseOverrides: {selector: '#data-content', mode: 'inner'}})"
>{{ tab.label }}</a>
{% endfor %}
</nav>
<!-- Mobile tab selector -->
<div class="md:hidden">
<button
type="button"
class="flex w-full items-center justify-between p-3 text-sm font-medium text-stone-800"
data-on:click="$_tabsOpen = !$_tabsOpen"
>
<span>
{% for tab in tabs %}
<span data-show="$_activeTab === '{{ tab.key }}'">{{ tab.label }}</span>
{% endfor %}
</span>
<span data-show="!$_tabsOpen">{% call icons::chevron_down("h-5 w-5") %}</span>
<span data-show="$_tabsOpen" style="display:none">{% call icons::chevron_up("h-5 w-5") %}</span>
</button>
<div class="flex flex-col border-t" data-show="$_tabsOpen" style="display:none">
{% for tab in tabs %}
<a
href="/data?type={{ tab.key }}"
data-class:bg-accent-subtle="$_activeTab === '{{ tab.key }}'"
data-class:text-stone-800="$_activeTab === '{{ tab.key }}'"
data-class:font-semibold="$_activeTab === '{{ tab.key }}'"
data-class:text-stone-600="$_activeTab !== '{{ tab.key }}'"
class="px-3 py-2 text-sm transition hover:bg-surface-alt"
data-on:click__prevent="$_activeTab = '{{ tab.key }}'; $_tabsOpen = false; history.pushState(null, '', '/data?type={{ tab.key }}'); @get('/data?type={{ tab.key }}', {responseOverrides: {selector: '#data-content', mode: 'inner'}})"
>{{ tab.label }}</a>
{% endfor %}
</div>
</div>
<!-- Search -->
<div class="border-t px-3 py-2">
<input
type="search"
id="data-search"
placeholder="Search..."
value="{{ search_value }}"
class="input-field w-full text-sm"
{% if !search_value.is_empty() %}data-init="el.focus(); el.selectionStart = el.value.length"{% endif %}
/>
</div>
</div>
<div id="data-content" class="data-page-content">
{{ content|safe }}
</div>
<div id="detail-panel"></div>
<script>
(() => {
const searchInput = document.getElementById('data-search');
if (!searchInput) return;
let debounceTimer;
searchInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const params = new URLSearchParams(window.location.search);
const activeTab = params.get('type') || 'brews';
const query = searchInput.value;
const url = `/data?type=${activeTab}&q=${encodeURIComponent(query)}`;
history.pushState(null, '', url);
fetch(url, { headers: { 'datastar-request': 'true' } })
.then((res) => res.text())
.then((html) => {
document.getElementById('data-content').innerHTML = html;
});
}, 300);
});
})();
</script>
{% endblock %}