build(deps): bump askama from 0.12.1 to 0.15.4 (#7)

* build(deps): bump askama from 0.12.1 to 0.15.4

Bumps [askama](https://github.com/askama-rs/askama) from 0.12.1 to 0.15.4.
- [Release notes](https://github.com/askama-rs/askama/releases)
- [Commits](https://github.com/askama-rs/askama/compare/0.12.1...v0.15.4)

---
updated-dependencies:
- dependency-name: askama
  dependency-version: 0.15.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): adapt templates and tests for askama 0.15

- Migrate macro calls from {% call %} to {{ }} expression syntax (134 occurrences)
- Update test assertion for askama 0.15's numeric HTML entity encoding (&#38; vs &amp;)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Seager <jon@sgrs.uk>
This commit is contained in:
dependabot[bot] 2026-02-06 18:45:34 +00:00 committed by GitHub
parent 5bba529936
commit ea4093b699
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 166 additions and 174 deletions

74
Cargo.lock generated
View file

@ -103,46 +103,54 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "askama"
version = "0.12.1"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57"
dependencies = [
"askama_derive",
"askama_escape",
"humansize",
"num-traits",
"askama_macros",
"itoa",
"percent-encoding",
"serde",
"serde_json",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"memchr",
"proc-macro2",
"quote",
"rustc-hash",
"serde",
"serde_derive",
"syn 2.0.114",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
name = "askama_macros"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b"
dependencies = [
"askama_derive",
]
[[package]]
name = "askama_parser"
version = "0.2.1"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c"
dependencies = [
"nom",
"rustc-hash",
"serde",
"serde_derive",
"unicode-ident",
"winnow",
]
[[package]]
@ -1149,15 +1157,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "hyper"
version = "1.8.1"
@ -1543,16 +1542,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -3096,12 +3085,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@ -3724,6 +3707,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
[[package]]
name = "wiremock"
version = "0.6.5"

View file

@ -7,7 +7,7 @@ edition = "2024"
anyhow = "1.0"
async-trait = "0.1"
axum = { version = "0.7", features = ["macros"] }
askama = "0.12"
askama = "0.15"
base64 = "0.22"
chrono = { version = "0.4", features = ["serde", "clock"] }
clap = { version = "4.5", features = ["derive", "env"] }

View file

@ -21,7 +21,7 @@
{% for passkey in passkeys %}
<div class="flex items-center justify-between gap-4 rounded-md bg-surface-alt px-4 py-3">
<div class="flex items-center gap-3 min-w-0">
{% call icons::key("h-4 w-4 text-accent shrink-0") %}
{{ icons::key("h-4 w-4 text-accent shrink-0") }}
<div class="min-w-0">
<span class="block text-sm font-semibold text-text">{{ passkey.name }}</span>
<span class="block text-xs text-text-muted">
@ -37,7 +37,7 @@
onclick="deletePasskey({{ passkey.id }}, '{{ passkey.name }}')"
aria-label="Delete passkey"
>
{% call icons::delete("h-4 w-4") %}
{{ icons::delete("h-4 w-4") }}
</button>
{% endif %}
</div>
@ -80,7 +80,7 @@
</button>
</div>
<div id="add-passkey-loading" class="mt-3 hidden flex items-center gap-3 text-sm text-accent">
{% call icons::spinner("h-5 w-5") %}
{{ icons::spinner("h-5 w-5") }}
Follow the prompts from your browser or device...
</div>
</div>
@ -132,7 +132,7 @@
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
aria-label="Revoke token"
>
{% call icons::delete("h-4 w-4") %}
{{ icons::delete("h-4 w-4") }}
</button>
</div>
{% endfor %}

View file

@ -2,7 +2,7 @@
{% block head %}
<script>
{% call location::location_search_js() %}
{{ location::location_search_js() }}
</script>
{% endblock %}
@ -35,7 +35,7 @@
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/extract-bag-scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_scanExtracted = true; document.getElementById('scan-extract-form').reset() } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Extraction failed. Please try again.' }"
>
{% call scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") %}
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") }}
</form>
</div>
@ -427,14 +427,14 @@
<!-- Location search -->
<div class="mt-4 border-b pb-4" data-show="!$_reviewingCafe">
{% call location::location_search("$_cafeError") %}
{{ location::location_search("$_cafeError") }}
<p data-show="$_cafeError" data-text="$_cafeError" style="display:none" class="mt-2 text-sm text-red-600"></p>
</div>
<!-- Selected cafe indicator -->
<div class="mt-4 border-b pb-4 flex items-center justify-between" data-show="$_reviewingCafe" style="display:none">
<div class="flex items-center gap-2">
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
{{ icons::location("h-4 w-4 text-accent shrink-0") }}
<span class="text-sm font-medium text-text" data-text="$_cafeName"></span>
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display:none"></span>
</div>

View file

@ -2,7 +2,7 @@
{% block head %}
<script>
{% call location::location_search_js() %}
{{ location::location_search_js() }}
</script>
{% endblock %}
@ -88,7 +88,7 @@
style="display: none"
>
<div class="flex items-center gap-2">
{% call icons::location("h-4 w-4 text-accent shrink-0") %}
{{ icons::location("h-4 w-4 text-accent shrink-0") }}
<span class="font-medium text-text" data-text="$_cafeName"></span>
<span class="text-xs text-text-muted" data-show="$_cafeCity" data-text="$_cafeCity" style="display: none"></span>
</div>
@ -106,7 +106,7 @@
style="display: none"
>
<div class="flex items-center gap-2">
{% call icons::bag("h-4 w-4 text-accent shrink-0") %}
{{ icons::bag("h-4 w-4 text-accent shrink-0") }}
<span class="font-medium text-text" data-text="$_roastName"></span>
<span class="text-xs text-text-muted" data-show="$_roasterName" data-text="$_roasterName" style="display: none"></span>
</div>
@ -129,7 +129,7 @@
<div class="mt-4" data-show="$_step === 1" style="display: none">
<div class="rounded-lg border bg-surface p-5">
<div data-show="!$_reviewingCafe">
{% call location::location_search("$_error") %}
{{ location::location_search("$_error") }}
</div>
<!-- Review form for new cafe from Foursquare -->
@ -208,7 +208,7 @@
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_roastName = $_scanSuccess; $_step = 3 } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Scan failed. Please try again.' }"
>
{% call scan::scan_input("checkin-scan-form", "checkin-image", "$_extracting", "$_extractError", "Describe the coffee…", "Scanning…") %}
{{ scan::scan_input("checkin-scan-form", "checkin-image", "$_extracting", "$_extractError", "Describe the coffee…", "Scanning…") }}
</form>
</div>

View file

@ -5,7 +5,7 @@
<div class="rounded-lg border bg-surface p-6">
{% if token.is_some() %}
<div class="flex flex-col items-center text-center gap-3">
{% call icons::check_circle("h-10 w-10 text-emerald-600 dark:text-emerald-400") %}
{{ icons::check_circle("h-10 w-10 text-emerald-600 dark:text-emerald-400") }}
<h1 class="text-2xl font-semibold text-accent">CLI Authenticated</h1>
<p class="text-sm text-text-secondary">
Your CLI has been authenticated successfully. You can close this window and return to the terminal.
@ -13,7 +13,7 @@
</div>
{% else if error.is_some() %}
<div class="flex flex-col items-center text-center gap-3">
{% call icons::x_circle("h-10 w-10 text-red-600 dark:text-red-400") %}
{{ icons::x_circle("h-10 w-10 text-red-600 dark:text-red-400") }}
<h1 class="text-2xl font-semibold text-accent">Authentication Failed</h1>
<div class="w-full rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800 dark:bg-red-950/40 dark:border-red-800 dark:text-red-300">
{{ error.as_ref().unwrap() }}
@ -21,7 +21,7 @@
</div>
{% else %}
<div class="flex flex-col items-center text-center gap-3">
{% call icons::spinner("h-10 w-10") %}
{{ icons::spinner("h-10 w-10") }}
<h1 class="text-2xl font-semibold text-accent">CLI Authentication</h1>
<p class="text-sm text-text-secondary">
Processing authentication&hellip;

View file

@ -10,7 +10,7 @@
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") %}
{{ icons::plus_circle("h-4 w-4") }}
Add
</a>
{% endif %}

View file

@ -57,7 +57,7 @@
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/extract-bag-scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_scanExtracted = true; document.getElementById('scan-extract-form').reset() } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Extraction failed. Please try again.' }"
>
{% call scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") %}
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") }}
</form>
</div>
@ -128,7 +128,7 @@
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-accent transition hover:text-accent-hover hover:bg-surface-alt"
title="Brew Again"
>
{% call icons::refresh("h-4 w-4") %}
{{ icons::refresh("h-4 w-4") }}
</button>
</form>
{% endif %}
@ -148,7 +148,7 @@
</div>
<div id="open-bags-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for bag in open_bags %}
{% call bag_card::card(bag, is_authenticated) %}
{{ bag_card::card(bag, is_authenticated) }}
{% endfor %}
</div>
</section>
@ -182,37 +182,37 @@
</div>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-7">
<a href="/data?type=brews" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::beaker("h-6 w-6 text-accent") %}
{{ icons::beaker("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.brews }}</span>
<span class="text-xs text-text-muted">Brews</span>
</a>
<a href="/data?type=roasts" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::fire("h-6 w-6 text-accent") %}
{{ icons::fire("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasts }}</span>
<span class="text-xs text-text-muted">Roasts</span>
</a>
<a href="/data?type=roasters" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::building("h-6 w-6 text-accent") %}
{{ icons::building("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasters }}</span>
<span class="text-xs text-text-muted">Roasters</span>
</a>
<a href="/data?type=bags" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::bag("h-6 w-6 text-accent") %}
{{ icons::bag("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.bags }}</span>
<span class="text-xs text-text-muted">Bags</span>
</a>
<a href="/data?type=cups" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::cup("h-6 w-6 text-accent") %}
{{ icons::cup("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cups }}</span>
<span class="text-xs text-text-muted">Cups</span>
</a>
<a href="/data?type=cafes" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::location("h-6 w-6 text-accent") %}
{{ icons::location("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cafes }}</span>
<span class="text-xs text-text-muted">Cafes</span>
</a>
<a href="/data?type=gear" class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt">
{% call icons::wrench("h-6 w-6 text-accent") %}
{{ icons::wrench("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.gear }}</span>
<span class="text-xs text-text-muted">Gear</span>
</a>

View file

@ -12,7 +12,7 @@
{% if is_authenticated %}
<div class="mt-3 pt-3 border-t flex gap-3">
<a href="/add?type=brew&bag_id={{ bag.id }}" class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
{% call icons::plus_circle("h-4 w-4") %}
{{ icons::plus_circle("h-4 w-4") }}
Brew
</a>
<button
@ -20,7 +20,7 @@
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-text"
onclick="closeBag('{{ bag.id }}', document.getElementById('bag-card-{{ bag.id }}'))"
>
{% call icons::x_circle("h-4 w-4") %}
{{ icons::x_circle("h-4 w-4") }}
Close
</button>
</div>

View file

@ -16,7 +16,7 @@
<h3 class="text-base font-semibold text-text">Roaster</h3>
<div class="mt-2 rounded-lg border bg-surface px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-2">
{% call icons::fire("h-4 w-4 text-accent shrink-0") %}
{{ icons::fire("h-4 w-4 text-accent shrink-0") }}
<span class="font-medium text-text" data-text="$_roasterName"></span>
<span class="text-xs text-text-muted" data-show="$_roasterCountry" data-text="$_roasterCountry" style="display:none"></span>
</div>
@ -53,7 +53,7 @@
<h3 class="text-base font-semibold text-text">Roast</h3>
<div class="mt-2 rounded-lg border bg-surface px-4 py-3 flex items-center justify-between">
<div class="flex items-center gap-2">
{% call icons::bag("h-4 w-4 text-accent shrink-0") %}
{{ icons::bag("h-4 w-4 text-accent shrink-0") }}
<span class="font-medium text-text" data-text="$_roastName"></span>
<span class="text-xs text-text-muted" data-show="$_origin" data-text="$_origin" style="display:none"></span>
</div>
@ -107,7 +107,7 @@
</div>
<p data-show="$_scanError" data-text="$_scanError" style="display:none" class="text-sm text-red-600"></p>
<div data-show="$_scanSubmitting" style="display:none" class="flex items-center gap-3 text-sm text-accent">
{% call icons::spinner("h-5 w-5") %}
{{ icons::spinner("h-5 w-5") }}
Saving&hellip;
</div>
<div data-show="!$_scanSubmitting" class="flex items-center justify-end gap-2">

View file

@ -18,16 +18,16 @@
<section class="rounded-lg border bg-surface"
{% if bags.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(bags.next_page().unwrap())|safe }}" data-target="#bag-list"{% endif %}
>
{% call table::search_header(navigator, "#bag-list") %}
{{ table::search_header(navigator, "#bag-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#bag-list") %}
{% call table::sortable_header("Roaster", "roaster", navigator, "#bag-list") %}
{% call table::sortable_header("Roast", "roast", navigator, "#bag-list") %}
{% call table::sortable_header("Status", "status", navigator, "#bag-list") %}
{% call table::sortable_header("Finished", "finished-at", navigator, "#bag-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#bag-list") }}
{{ table::sortable_header("Roaster", "roaster", navigator, "#bag-list") }}
{{ table::sortable_header("Roast", "roast", navigator, "#bag-list") }}
{{ table::sortable_header("Status", "status", navigator, "#bag-list") }}
{{ table::sortable_header("Finished", "finished-at", navigator, "#bag-list") }}
{% if is_authenticated %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
@ -83,13 +83,13 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
{% call icons::x_circle("h-4 w-4") %} Close bag
{{ icons::x_circle("h-4 w-4") }} Close bag
</button>
{% endif %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -103,8 +103,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</div>
</td>
@ -118,13 +118,13 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
{% call icons::x_circle("h-4 w-4") %} Close bag
{{ icons::x_circle("h-4 w-4") }} Close bag
</button>
{% endif %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -137,7 +137,7 @@
{% if bags.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No bags match your search.</div>
{% endif %}
{% call table::pagination_header(bags, navigator, "#bag-list") %}
{{ table::pagination_header(bags, navigator, "#bag-list") }}
{% if bags.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -17,13 +17,13 @@
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
<button type="submit"
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
{% call icons::plus_circle("h-4 w-4") %} Brew Again
{{ icons::plus_circle("h-4 w-4") }} Brew Again
</button>
</form>
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
{% endmacro %}
@ -45,13 +45,13 @@
<section class="rounded-lg border bg-surface"
{% if brews.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(brews.next_page().unwrap())|safe }}" data-target="#brew-list"{% endif %}
>
{% call table::search_header(navigator, "#brew-list") %}
{{ table::search_header(navigator, "#brew-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#brew-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#brew-list") }}
<th scope="col" class="px-4 py-3">Coffee</th>
<th scope="col" class="px-4 py-3">Grind</th>
<th scope="col" class="px-4 py-3">Recipe</th>
@ -109,14 +109,14 @@
</td>
{% if is_authenticated %}
<td data-label="" class="card-detail hidden">
{% call brew_actions(brew, navigator) %}
{{ brew_actions(brew, navigator) }}
</td>
<td data-label="" class="card-actions px-4 py-3 text-right">
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</td>
{% endif %}
@ -124,7 +124,7 @@
{% if is_authenticated %}
<tr class="hidden detail-row">
<td colspan="7" class="bg-surface-alt px-4 py-2">
{% call brew_actions(brew, navigator) %}
{{ brew_actions(brew, navigator) }}
</td>
</tr>
{% endif %}
@ -139,7 +139,7 @@
</div>
{% endif %}
{% call table::pagination_header(brews, navigator, "#brew-list") %}
{{ table::pagination_header(brews, navigator, "#brew-list") }}
{% if brews.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>

View file

@ -18,15 +18,15 @@
<section class="rounded-lg border bg-surface"
{% if cafes.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(cafes.next_page().unwrap())|safe }}" data-target="#cafe-list"{% endif %}
>
{% call table::search_header(navigator, "#cafe-list") %}
{{ table::search_header(navigator, "#cafe-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#cafe-list") %}
{% call table::sortable_header("Name", "name", navigator, "#cafe-list") %}
{% call table::sortable_header("City", "city", navigator, "#cafe-list") %}
{% call table::sortable_header("Country", "country", navigator, "#cafe-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#cafe-list") }}
{{ table::sortable_header("Name", "name", navigator, "#cafe-list") }}
{{ table::sortable_header("City", "city", navigator, "#cafe-list") }}
{{ table::sortable_header("Country", "country", navigator, "#cafe-list") }}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
</tr>
</thead>
@ -56,19 +56,19 @@
<div class="flex items-center gap-4">
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::location("h-4 w-4") %} View Map
{{ icons::location("h-4 w-4") }} View Map
</a>
{% if cafe.has_website %}
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::external_link("h-4 w-4") %} Homepage
{{ icons::external_link("h-4 w-4") }} Homepage
</a>
{% endif %}
{% if is_authenticated %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
{% endif %}
</div>
@ -77,8 +77,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</td>
</tr>
@ -87,19 +87,19 @@
<div class="flex items-center gap-4">
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::location("h-4 w-4") %} View Map
{{ icons::location("h-4 w-4") }} View Map
</a>
{% if cafe.has_website %}
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::external_link("h-4 w-4") %} Homepage
{{ icons::external_link("h-4 w-4") }} Homepage
</a>
{% endif %}
{% if is_authenticated %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
{% endif %}
</div>
@ -112,7 +112,7 @@
{% if cafes.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No cafes match your search.</div>
{% endif %}
{% call table::pagination_header(cafes, navigator, "#cafe-list") %}
{{ table::pagination_header(cafes, navigator, "#cafe-list") }}
{% if cafes.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -18,16 +18,16 @@
<section class="rounded-lg border bg-surface"
{% if cups.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(cups.next_page().unwrap())|safe }}" data-target="#cup-list"{% endif %}
>
{% call table::search_header(navigator, "#cup-list") %}
{{ table::search_header(navigator, "#cup-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#cup-list") %}
{% call table::sortable_header("Roast", "roast", navigator, "#cup-list") %}
{% call table::sortable_header("Roaster", "roaster", navigator, "#cup-list") %}
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
{% call table::sortable_header("City", "city", navigator, "#cup-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#cup-list") }}
{{ table::sortable_header("Roast", "roast", navigator, "#cup-list") }}
{{ table::sortable_header("Roaster", "roaster", navigator, "#cup-list") }}
{{ table::sortable_header("Cafe", "cafe", navigator, "#cup-list") }}
{{ table::sortable_header("City", "city", navigator, "#cup-list") }}
{% if is_authenticated %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
@ -61,7 +61,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -69,8 +69,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</td>
{% endif %}
@ -82,7 +82,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -95,7 +95,7 @@
{% if cups.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No cups match your search.</div>
{% endif %}
{% call table::pagination_header(cups, navigator, "#cup-list") %}
{{ table::pagination_header(cups, navigator, "#cup-list") }}
{% if cups.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -18,16 +18,16 @@
<section class="rounded-lg border bg-surface"
{% if gear.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(gear.next_page().unwrap())|safe }}" data-target="#gear-list"{% endif %}
>
{% call table::search_header(navigator, "#gear-list") %}
{{ table::search_header(navigator, "#gear-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#gear-list") %}
{% call table::sortable_header("Category", "category", navigator, "#gear-list") %}
{% call table::sortable_header("Make", "make", navigator, "#gear-list") %}
{% call table::sortable_header("Model", "model", navigator, "#gear-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#gear-list") }}
{{ table::sortable_header("Category", "category", navigator, "#gear-list") }}
{{ table::sortable_header("Make", "make", navigator, "#gear-list") }}
{{ table::sortable_header("Model", "model", navigator, "#gear-list") }}
{% if is_authenticated %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
{% endif %}
@ -58,7 +58,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -66,8 +66,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</td>
{% endif %}
@ -79,7 +79,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -92,7 +92,7 @@
{% if gear.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No gear matches your search.</div>
{% endif %}
{% call table::pagination_header(gear, navigator, "#gear-list") %}
{{ table::pagination_header(gear, navigator, "#gear-list") }}
{% if gear.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -18,14 +18,14 @@
<section class="rounded-lg border bg-surface"
{% if roasts.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(roasts.next_page().unwrap())|safe }}" data-target="#roast-list"{% endif %}
>
{% call table::search_header(navigator, "#roast-list") %}
{{ table::search_header(navigator, "#roast-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#roast-list") %}
{% call table::sortable_header("Roast", "name", navigator, "#roast-list") %}
{% call table::sortable_header("Origin", "origin", navigator, "#roast-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#roast-list") }}
{{ table::sortable_header("Roast", "name", navigator, "#roast-list") }}
{{ table::sortable_header("Origin", "origin", navigator, "#roast-list") }}
<th scope="col" class="px-4 py-3">Tasting Notes</th>
{% if is_authenticated %}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
@ -79,7 +79,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -87,8 +87,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
</td>
{% endif %}
@ -100,7 +100,7 @@
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
</div>
</td>
@ -113,7 +113,7 @@
{% if roasts.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No roasts match your search.</div>
{% endif %}
{% call table::pagination_header(roasts, navigator, "#roast-list") %}
{{ table::pagination_header(roasts, navigator, "#roast-list") }}
{% if roasts.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -18,15 +18,15 @@
<section class="rounded-lg border bg-surface"
{% if roasters.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(roasters.next_page().unwrap())|safe }}" data-target="#roaster-list"{% endif %}
>
{% call table::search_header(navigator, "#roaster-list") %}
{{ table::search_header(navigator, "#roaster-list") }}
<div class="overflow-x-auto">
<table class="responsive-table min-w-full divide-y text-left text-sm text-text">
<thead class="bg-surface-alt text-xs font-semibold tracking-wide text-text">
<tr>
{% call table::sortable_header("Added", "created-at", navigator, "#roaster-list") %}
{% call table::sortable_header("Name", "name", navigator, "#roaster-list") %}
{% call table::sortable_header("Country", "country", navigator, "#roaster-list") %}
{% call table::sortable_header("City", "city", navigator, "#roaster-list") %}
{{ table::sortable_header("Added", "created-at", navigator, "#roaster-list") }}
{{ table::sortable_header("Name", "name", navigator, "#roaster-list") }}
{{ table::sortable_header("Country", "country", navigator, "#roaster-list") }}
{{ table::sortable_header("City", "city", navigator, "#roaster-list") }}
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
</tr>
</thead>
@ -60,14 +60,14 @@
{% if roaster.has_homepage %}
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::external_link("h-4 w-4") %} Homepage
{{ icons::external_link("h-4 w-4") }} Homepage
</a>
{% endif %}
{% if is_authenticated %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
{% endif %}
</div>
@ -78,8 +78,8 @@
<button type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
title="Actions">
<span class="icon-expand">{% call icons::ellipsis_vertical("h-5 w-5") %}</span>
<span class="icon-collapse hidden">{% call icons::chevron_up("h-5 w-5") %}</span>
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
</button>
{% endif %}
</td>
@ -91,14 +91,14 @@
{% if roaster.has_homepage %}
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
{% call icons::external_link("h-4 w-4") %} Homepage
{{ icons::external_link("h-4 w-4") }} Homepage
</a>
{% endif %}
{% if is_authenticated %}
<button type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
{% call icons::delete("h-4 w-4") %} Delete
{{ icons::delete("h-4 w-4") }} Delete
</button>
{% endif %}
</div>
@ -112,7 +112,7 @@
{% if roasters.items.is_empty() %}
<div class="p-8 text-center text-text-muted">No roasters match your search.</div>
{% endif %}
{% call table::pagination_header(roasters, navigator, "#roaster-list") %}
{{ table::pagination_header(roasters, navigator, "#roaster-list") }}
{% if roasters.has_next() %}
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
{% endif %}

View file

@ -43,8 +43,8 @@ const locateUser = (triggerEl) => {
data-attr:disabled="$_locating || $_locationFound"
aria-label="Use my location"
>
<span data-show="!$_locating">{% call icons::location("h-5 w-5") %}</span>
<span data-show="$_locating" style="display: none">{% call icons::spinner("h-5 w-5") %}</span>
<span data-show="!$_locating">{{ icons::location("h-5 w-5") }}</span>
<span data-show="$_locating" style="display: none">{{ icons::spinner("h-5 w-5") }}</span>
</button>
<input type="text" data-bind:_city-name
class="input-field w-full text-sm"

View file

@ -4,41 +4,41 @@
<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">
<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">
{% call icons::home("h-5 w-5") %}
{{ icons::home("h-5 w-5") }}
</a>
<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">{% call icons::sun("h-5 w-5") %}</span>
<span class="theme-icon-dark hidden">{% call icons::moon("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>
</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">{% call icons::hamburger("h-5 w-5") %}</span>
<span data-show="$_navOpen" style="display:none">{% call icons::close("h-5 w-5") %}</span>
<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 {% call icons::squares("h-4 w-4 shrink-0") %}
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 {% call icons::timeline("h-4 w-4 shrink-0") %}
Timeline {{ icons::timeline("h-4 w-4 shrink-0") }}
</a>
{% 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">
Check In {% call icons::checkin("h-4 w-4 shrink-0") %}
Check In {{ icons::checkin("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 == "account" %}text-accent font-medium{% else %}text-text-muted hover:text-text hover:bg-surface-alt{% endif %}" href="/account">
Admin {% call icons::cog("h-4 w-4 shrink-0") %}
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 {% call icons::logout("h-4 w-4 shrink-0") %}
Sign Out {{ icons::logout("h-4 w-4 shrink-0") }}
</button>
</form>
{% 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">
Login {% call icons::user("h-4 w-4 shrink-0") %}
Login {{ icons::user("h-4 w-4 shrink-0") }}
</a>
{% endif %}
</div>

View file

@ -7,7 +7,7 @@
class="shrink-0 inline-flex items-center justify-center rounded-md border p-2.5 text-accent transition hover:bg-surface-alt cursor-pointer"
aria-label="Take Photo"
>
{% call icons::camera("h-5 w-5") %}
{{ icons::camera("h-5 w-5") }}
</brew-photo-capture>
<input
type="text"
@ -17,7 +17,7 @@
/>
</div>
<div data-show="{{ extracting_signal }}" style="display:none" class="flex items-center gap-3 text-sm text-accent">
{% call icons::spinner("h-5 w-5") %}
{{ icons::spinner("h-5 w-5") }}
{{ spinner_text }}
</div>
<p data-show="{{ error_signal }}" data-text="{{ error_signal }}" style="display:none" class="mt-2 text-sm text-red-600"></p>

View file

@ -26,8 +26,8 @@
<span data-show="{{ tab_signal_js }} === '{{ 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>
<span data-show="!$_tabsOpen">{{ icons::chevron_down("h-5 w-5") }}</span>
<span data-show="$_tabsOpen" style="display:none">{{ 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 %}

View file

@ -2,7 +2,7 @@
<h2 id="{{ month.anchor }}" class="timeline-heading scroll-mt-24 mb-6 text-2xl font-semibold text-text" data-timeline-month>
{{ month.heading }}
<button type="button" class="timeline-top-btn" onclick="window.scrollTo({top: 0, behavior: 'smooth'})" aria-label="Back to top">
{% call icons::arrow_up("h-5 w-5") %}
{{ icons::arrow_up("h-5 w-5") }}
</button>
</h2>
{% for event in month.events %}
@ -33,7 +33,7 @@
rel="noreferrer noopener"
aria-label="Open external link"
>
{% call icons::external_link("h-4 w-4") %}
{{ icons::external_link("h-4 w-4") }}
<span class="sr-only">Open external link</span>
</a>
{% endif %}
@ -59,7 +59,7 @@
title="Brew again"
>
<span class="sr-only">Brew again</span>
{% call icons::refresh("h-4 w-4") %}
{{ icons::refresh("h-4 w-4") }}
</button>
</form>
{% endif %}
@ -74,7 +74,7 @@
<dd class="text-right flex items-center justify-end gap-1.5">
{{ detail.value }}
<a href="{{ detail.link.as_ref().unwrap() }}" target="_blank" rel="noreferrer noopener" class="text-accent hover:text-accent-hover" aria-label="View on map">
{% call icons::map("h-4 w-4") %}
{{ icons::map("h-4 w-4") }}
</a>
</dd>
{% else if let Some(url) = detail.link %}

View file

@ -258,7 +258,7 @@ async fn timeline_page_signals_more_results_when_over_page_size() {
assert!(
body.contains(
"data-next-url=\"/timeline?page=2&amp;page_size=5&amp;sort=occurred-at&amp;dir=desc\""
"data-next-url=\"/timeline?page=2&#38;page_size=5&#38;sort=occurred-at&#38;dir=desc\""
),
"Expected loader next-page URL missing from timeline HTML:\n{}",
body