feat(icons): add icon macros, remap entity types, add icons to tabs and timeline

- Add new icon macros: coffee_bean, grinder, cup (redesigned), pencil,
  clipboard, check, x_mark, arrow_down_tray, arrow_up_tray
- Remove orphaned building and wrench macros
- Remap entity icons: roast→coffee_bean, roaster→fire, gear→grinder
- Replace pill labels with entity-type icons in homepage activity rows
- Add entity-type icons to timeline cards and tab buttons
- Fix brew/bag card action icons (cup→beaker)
- Add entity_type field to TimelineEventView, add roaster detail picks
This commit is contained in:
Jon Seager 2026-02-07 22:36:48 +00:00
parent af9344202d
commit ce4f32c63f
No known key found for this signature in database
7 changed files with 64 additions and 24 deletions

View file

@ -46,6 +46,7 @@ pub struct TimelineBrewDataView {
#[derive(Clone)]
pub struct TimelineEventView {
pub id: String,
pub entity_type: String,
pub kind_label: &'static str,
pub date_label: String,
pub relative_date_label: String,
@ -137,6 +138,7 @@ impl TimelineEventView {
Self {
id: id.to_string(),
entity_type,
kind_label,
date_label: occurred_at.format("%B %d, %Y").to_string(),
relative_date_label: relative_date(occurred_at),
@ -164,6 +166,7 @@ impl TimelineEventView {
let picks: &[&str] = match entity_type {
"brew" => &["Roaster", "Brewer"],
"roast" => &["Roaster", "Origin"],
"roaster" => &["City", "Country"],
"cafe" => &["City", "Country"],
"cup" => &["Roaster", "Cafe"],
_ => &[],

View file

@ -192,14 +192,14 @@ is_authenticated %}
{% for event in recent_events %}
<div class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="pill pill-muted w-28 shrink-0 justify-center whitespace-nowrap"
>{{ event.kind_label }}</span
>
<span class="text-text-muted shrink-0">
{% if event.entity_type == "brew" %}{{ icons::beaker("h-5 w-5") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-5 w-5") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-5 w-5") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-5 w-5") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-5 w-5") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-5 w-5") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-5 w-5") }}{% else %}{{ icons::beaker("h-5 w-5") }}{% endif %}
</span>
<p class="min-w-0 truncate text-sm">
<a href="{{ event.link }}" class="font-medium text-text hover:text-accent"
>{{ event.title }}</a
>{% if let Some(sub) = event.subtitle %}
<span class="text-xs text-text-muted italic">· {{ sub }}</span>{% endif %}
<span class="text-xs text-text-muted">· {{ sub }}</span>{% endif %}
</p>
</div>
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
@ -214,7 +214,7 @@ is_authenticated %}
{% for _ in 0..3 %}
<div class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<div class="h-5 w-28 shrink-0 rounded-full bg-surface-alt"></div>
<div class="h-5 w-5 shrink-0 rounded-full bg-surface-alt"></div>
<div class="h-4 w-40 rounded bg-surface-alt"></div>
</div>
<div class="h-3 w-16 shrink-0 rounded bg-surface-alt"></div>
@ -246,7 +246,7 @@ is_authenticated %}
href="/data?type=roasts"
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
>
{{ icons::fire("h-6 w-6 text-accent") }}
{{ icons::coffee_bean("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text">{{ stats.roasts }}</span>
<span class="text-sm font-medium text-accent">Roasts</span>
</a>
@ -254,7 +254,7 @@ is_authenticated %}
href="/data?type=roasters"
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
>
{{ icons::building("h-6 w-6 text-accent") }}
{{ icons::fire("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text">{{ stats.roasters }}</span>
<span class="text-sm font-medium text-accent">Roasters</span>
</a>
@ -286,7 +286,7 @@ is_authenticated %}
href="/data?type=gear"
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
>
{{ icons::wrench("h-6 w-6 text-accent") }}
{{ icons::grinder("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text">{{ stats.gear }}</span>
<span class="text-sm font-medium text-accent">Gear</span>
</a>

View file

@ -17,7 +17,7 @@
<div class="mt-3 flex items-center gap-2">
<a href="/add?type=brew&bag_id={{ bag.id }}"
class="flex-1 inline-flex h-8 items-center justify-center gap-1.5 rounded-md border px-2 text-sm font-medium text-accent transition hover:text-accent-hover hover:bg-surface-alt">
{{ icons::cup("h-4 w-4") }}
{{ icons::beaker("h-4 w-4") }}
Brew
</a>
<button

View file

@ -16,7 +16,7 @@
<div class="mt-3">
<a href="{{ brew.brew_again_url() }}"
class="inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-md border px-2 text-sm font-medium text-accent transition hover:text-accent-hover hover:bg-surface-alt">
{{ icons::cup("h-4 w-4") }}
{{ icons::beaker("h-4 w-4") }}
Brew Again
</a>
</div>

View file

@ -55,8 +55,9 @@
{% endmacro %}
{% macro cup(class) %}
<svg class="{{ class }}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M2 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2h2a3 3 0 0 1 0 6h-1.1A5.002 5.002 0 0 1 14 16H6a5 5 0 0 1-5-5V6Zm16 4h2a1 1 0 1 0 0-2h-2v2ZM4 20a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Z" clip-rule="evenodd" />
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M7.25 2.75a.75.75 0 0 0-1.5 0C5.75 3.9 6.4 4.8 7 5.4c.35.35.5.7.5 1.1h-1a2.1 2.1 0 0 0-.8-1.6c-.45-.4-.95-1.1-.95-2.15ZM10.75 2.75a.75.75 0 0 0-1.5 0c0 1.15.65 2.05 1.25 2.65.35.35.5.7.5 1.1h-1a2.1 2.1 0 0 0-.8-1.6c-.45-.4-.95-1.1-.95-2.15Z" />
<path fill-rule="evenodd" d="M2.5 8.5A1.5 1.5 0 0 1 4 7h10a1.5 1.5 0 0 1 1.5 1.5v.5h.5a2.5 2.5 0 0 1 0 5h-.5v.5A3.5 3.5 0 0 1 12 18H6a3.5 3.5 0 0 1-3.5-3.5v-6Zm13 2v3h.5a1 1 0 1 0 0-2h-.25v-1h-.25ZM3.5 18.75a.75.75 0 0 1 .75-.75h9.5a.75.75 0 0 1 0 1.5h-9.5a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
@ -66,6 +67,20 @@
</svg>
{% endmacro %}
{% macro arrow_down_tray(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z" />
<path d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z" />
</svg>
{% endmacro %}
{% macro arrow_up_tray(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M9.25 13.25a.75.75 0 0 0 1.5 0V4.636l2.955 3.129a.75.75 0 0 0 1.09-1.03l-4.25-4.5a.75.75 0 0 0-1.09 0l-4.25 4.5a.75.75 0 1 0 1.09 1.03L9.25 4.636v8.614Z" />
<path d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z" />
</svg>
{% endmacro %}
{% macro plus_circle(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-11.25a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z" clip-rule="evenodd" />
@ -84,24 +99,27 @@
</svg>
{% endmacro %}
{% macro coffee_bean(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M9.75 2.5C6 3.3 3.5 6.3 3.5 10S6 16.7 9.75 17.5C9 16 8.25 13.5 8.25 10S9 4 9.75 2.5Z" />
<path d="M10.25 2.5C14 3.3 16.5 6.3 16.5 10S14 16.7 10.25 17.5C11 16 11.75 13.5 11.75 10S11 4 10.25 2.5Z" />
</svg>
{% endmacro %}
{% macro fire(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M13.5 4.938a7 7 0 1 1-9.006 1.737c.202-.257.59-.218.793.039.278.352.594.672.943.954.332.269.786-.049.773-.476a5.977 5.977 0 0 1 .572-2.759 6.026 6.026 0 0 1 2.486-2.665c.247-.14.55-.016.677.238A6.967 6.967 0 0 0 13.5 4.938ZM14 12a4 4 0 0 1-4 4c-1.913 0-3.52-1.398-3.91-3.182-.093-.429.44-.643.814-.413a4.043 4.043 0 0 0 1.601.564c.303.038.531-.24.51-.544a5.975 5.975 0 0 1 1.315-4.192.447.447 0 0 1 .431-.16A4.001 4.001 0 0 1 14 12Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro building(class) %}
{% macro grinder(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M2.879 7.121A3 3 0 0 0 7.5 6.66a2.997 2.997 0 0 0 2.5 1.34 2.997 2.997 0 0 0 2.5-1.34 3 3 0 1 0 4.622-3.78l-.293-.293A2 2 0 0 0 15.415 2H4.585a2 2 0 0 0-1.414.586l-.292.292a3 3 0 0 0 0 4.243ZM3 9.032a4.507 4.507 0 0 0 4.5-.29A4.48 4.48 0 0 0 10 9.5a4.48 4.48 0 0 0 2.5-.758 4.507 4.507 0 0 0 4.5.29V16.5h.25a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75v-3.5a.75.75 0 0 0-.75-.75h-2.5a.75.75 0 0 0-.75.75v3.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5H3V9.032Z" />
<path d="M3.5 3.5Q3.5 1.5 6 1.5h8Q16.5 1.5 16.5 3.5L13 6v9H7V6Z" />
<rect x="5" y="16.5" width="10" height="1.5" rx=".5" />
</svg>
{% endmacro %}
{% macro wrench(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M14.5 10a4.5 4.5 0 0 0 4.284-5.882c-.105-.324-.51-.391-.752-.15L15.34 6.66a.454.454 0 0 1-.493.11 3.01 3.01 0 0 1-1.618-1.616.455.455 0 0 1 .11-.494l2.694-2.692c.24-.241.174-.647-.15-.752a4.5 4.5 0 0 0-5.873 4.575c.055.873-.128 1.808-.8 2.368l-7.23 6.024a2.724 2.724 0 1 0 3.837 3.837l6.024-7.23c.56-.672 1.495-.855 2.368-.8.096.007.193.01.291.01ZM5 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
<path d="M14.5 11.5c.173 0 .345-.007.514-.022l3.754 3.754a2.5 2.5 0 0 1-3.536 3.536l-4.41-4.41 2.172-2.607c.052-.063.147-.138.342-.196.202-.06.469-.087.777-.067.128.008.257.012.387.012ZM6 4.586l2.33 2.33a.452.452 0 0 1-.08.09L6.8 8.214 4.586 6H3.309a.5.5 0 0 1-.447-.276l-1.7-3.402a.5.5 0 0 1 .093-.577l.49-.49a.5.5 0 0 1 .577-.094l3.402 1.7A.5.5 0 0 1 6 3.31v1.277Z" />
</svg>
{% endmacro %}
{% macro ellipsis_vertical(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@ -163,6 +181,24 @@
</svg>
{% endmacro %}
{% macro pencil(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.885L17.5 5.5a2.121 2.121 0 0 0-3-3L3.58 13.42a4 4 0 0 0-.885 1.343Z" />
</svg>
{% endmacro %}
{% macro clipboard(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M13.887 3.182c.396.037.79.08 1.183.128C16.194 3.45 17 4.414 17 5.517V16.75A2.25 2.25 0 0 1 14.75 19h-9.5A2.25 2.25 0 0 1 3 16.75V5.517c0-1.103.806-2.068 1.93-2.207.393-.048.787-.09 1.183-.128A3.001 3.001 0 0 1 9 1h2c1.373 0 2.531.923 2.887 2.182ZM7.5 4A1.5 1.5 0 0 1 9 2.5h2A1.5 1.5 0 0 1 12.5 4v.5h-5V4Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro check(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro x_mark(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />

View file

@ -10,7 +10,7 @@
class="tab"
data-class:tab-active="{{ tab_signal_js }} === '{{ tab.key }}'"
data-on:click="{{ tab_signal_js }} = '{{ tab.key }}'{% if !tab_base_url.is_empty() %}; history.pushState(null, '', '{{ tab_base_url }}{{ tab.key }}'); @get('{{ tab_base_url }}{{ tab.key }}', {responseOverrides: {selector: '{{ tab_fetch_target }}', mode: '{{ tab_fetch_mode }}'}}){% endif %}"
>{{ tab.label }}</button>
>{% if tab.key == "brew" || tab.key == "brews" %}{{ icons::beaker("h-4 w-4 shrink-0") }}{% elif tab.key == "roast" || tab.key == "roasts" %}{{ icons::coffee_bean("h-4 w-4 shrink-0") }}{% elif tab.key == "roaster" || tab.key == "roasters" %}{{ icons::fire("h-4 w-4 shrink-0") }}{% elif tab.key == "bag" || tab.key == "bags" %}{{ icons::bag("h-4 w-4 shrink-0") }}{% elif tab.key == "cup" || tab.key == "cups" %}{{ icons::cup("h-4 w-4 shrink-0") }}{% elif tab.key == "cafe" || tab.key == "cafes" %}{{ icons::location("h-4 w-4 shrink-0") }}{% elif tab.key == "gear" %}{{ icons::grinder("h-4 w-4 shrink-0") }}{% endif %} {{ tab.label }}</button>
{% endfor %}
</nav>
<!-- Mobile tab selector -->
@ -22,7 +22,7 @@
>
<span>
{% for tab in tabs %}
<span data-show="{{ tab_signal_js }} === '{{ tab.key }}'">{{ tab.label }}</span>
<span data-show="{{ tab_signal_js }} === '{{ tab.key }}'" class="inline-flex items-center gap-1.5">{% if tab.key == "brew" || tab.key == "brews" %}{{ icons::beaker("h-4 w-4 shrink-0") }}{% elif tab.key == "roast" || tab.key == "roasts" %}{{ icons::coffee_bean("h-4 w-4 shrink-0") }}{% elif tab.key == "roaster" || tab.key == "roasters" %}{{ icons::fire("h-4 w-4 shrink-0") }}{% elif tab.key == "bag" || tab.key == "bags" %}{{ icons::bag("h-4 w-4 shrink-0") }}{% elif tab.key == "cup" || tab.key == "cups" %}{{ icons::cup("h-4 w-4 shrink-0") }}{% elif tab.key == "cafe" || tab.key == "cafes" %}{{ icons::location("h-4 w-4 shrink-0") }}{% elif tab.key == "gear" %}{{ icons::grinder("h-4 w-4 shrink-0") }}{% endif %} {{ tab.label }}</span>
{% endfor %}
</span>
<span data-show="!$_tabsOpen">{{ icons::chevron_down("h-5 w-5") }}</span>
@ -35,7 +35,7 @@
class="tab-mobile"
data-class:tab-mobile-active="{{ tab_signal_js }} === '{{ tab.key }}'"
data-on:click="{{ tab_signal_js }} = '{{ tab.key }}'; $_tabsOpen = false{% if !tab_base_url.is_empty() %}; history.pushState(null, '', '{{ tab_base_url }}{{ tab.key }}'); @get('{{ tab_base_url }}{{ tab.key }}', {responseOverrides: {selector: '{{ tab_fetch_target }}', mode: '{{ tab_fetch_mode }}'}}){% endif %}"
>{{ tab.label }}</button>
>{% if tab.key == "brew" || tab.key == "brews" %}{{ icons::beaker("h-4 w-4 shrink-0") }}{% elif tab.key == "roast" || tab.key == "roasts" %}{{ icons::coffee_bean("h-4 w-4 shrink-0") }}{% elif tab.key == "roaster" || tab.key == "roasters" %}{{ icons::fire("h-4 w-4 shrink-0") }}{% elif tab.key == "bag" || tab.key == "bags" %}{{ icons::bag("h-4 w-4 shrink-0") }}{% elif tab.key == "cup" || tab.key == "cups" %}{{ icons::cup("h-4 w-4 shrink-0") }}{% elif tab.key == "cafe" || tab.key == "cafes" %}{{ icons::location("h-4 w-4 shrink-0") }}{% elif tab.key == "gear" %}{{ icons::grinder("h-4 w-4 shrink-0") }}{% endif %} {{ tab.label }}</button>
{% endfor %}
</div>
</div>

View file

@ -26,7 +26,8 @@
>
<div class="flex flex-wrap items-center justify-between gap-2">
{# Category — always visible #}
<span class="text-xs text-text-muted">
<span class="inline-flex items-center gap-1 text-xs text-text-muted">
{% if event.entity_type == "brew" %}{{ icons::beaker("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-3 w-3 shrink-0") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-3 w-3 shrink-0") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-3 w-3 shrink-0") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-3 w-3 shrink-0") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-3 w-3 shrink-0") }}{% endif %}
<span class="uppercase tracking-wide">{{ event.kind_label }}</span>
{# Relative date — shown when collapsed #}
<span class="tl-condensed uppercase tracking-wide" data-show="($_expandedCard !== '{{ event.id }}' && !$_expandedMonths.includes(',{{ month.anchor }}')) || $_collapsedCards.includes(',{{ event.id }}')"> · {{ event.relative_date_label }}</span>