Replace emoji icons with a monochrome SVG icon set

Emoji (📚 🔖 🏷️ ✎ ✕ 🕓  🤖 ⚠️ 🔍) rendered in each platform's own
colorful style and looked inconsistent. Add app/templates/_icons.html
with small inline SVG macros (Lucide-style, MIT-licensed path data,
stroke=currentColor) and use them throughout instead, so icons pick
up the muted/accent text colors like the rest of the UI.

The needs-review bookmark toggle now uses an outline vs. filled icon
for its two states instead of swapping emoji.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-07-20 22:50:52 +02:00
parent b6a236527e
commit dca6033c40
5 changed files with 97 additions and 20 deletions

51
app/templates/_icons.html Normal file
View File

@@ -0,0 +1,51 @@
{#
Gemeinsame Icons (angelehnt an Lucide, MIT-lizenziert), inline als SVG.
Monochrom über currentColor Farbe wird vom umgebenden Element gesetzt.
Verwendung: {% import "_icons.html" as icons %} ... {{ icons.edit() }}
#}
{% macro base(path, size=16) -%}
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="icon" aria-hidden="true">{{ path | safe }}</svg>
{%- endmacro %}
{% macro grid(size=16) -%}
{{ base('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>', size) }}
{%- endmacro %}
{% macro bookmark(filled=False, size=16) -%}
{{ base('<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"' + (' fill="currentColor"' if filled else '') + '/>', size) }}
{%- endmacro %}
{% macro edit(size=16) -%}
{{ base('<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/>', size) }}
{%- endmacro %}
{% macro trash(size=16) -%}
{{ base('<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>', size) }}
{%- endmacro %}
{% macro search(size=16) -%}
{{ base('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>', size) }}
{%- endmacro %}
{% macro plus(size=16) -%}
{{ base('<path d="M5 12h14"/><path d="M12 5v14"/>', size) }}
{%- endmacro %}
{% macro alert(size=16) -%}
{{ base('<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>', size) }}
{%- endmacro %}
{% macro sparkles(size=16) -%}
{{ base('<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>', size) }}
{%- endmacro %}
{% macro clock(size=16) -%}
{{ base('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', size) }}
{%- endmacro %}
{% macro spinner(size=16) -%}
{{ base('<path d="M21 12a9 9 0 1 1-6.219-8.56"/>', size) }}
{%- endmacro %}

View File

@@ -1,3 +1,4 @@
{% import "_icons.html" as icons %}
<div class="panel link-card {% if link.needs_review %}needs-review{% endif %}" id="link-{{ link.id }}"> <div class="panel link-card {% if link.needs_review %}needs-review{% endif %}" id="link-{{ link.id }}">
<div class="card-head"> <div class="card-head">
<span class="card-avatar" style="--h: {{ link.url | hue }};"> <span class="card-avatar" style="--h: {{ link.url | hue }};">
@@ -14,23 +15,22 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button class="ghost review-toggle {% if link.needs_review %}active{% endif %}" <button class="ghost icon-btn review-toggle {% if link.needs_review %}icon-bookmark-active{% endif %}"
style="padding:3px 7px; font-size:.8rem;"
title="{% if link.needs_review %}Nicht mehr zur Durchsicht{% else %}Zur Durchsicht markieren{% endif %}" title="{% if link.needs_review %}Nicht mehr zur Durchsicht{% else %}Zur Durchsicht markieren{% endif %}"
hx-post="/links/{{ link.id }}/toggle-review" hx-post="/links/{{ link.id }}/toggle-review"
hx-target="#link-{{ link.id }}" hx-target="#link-{{ link.id }}"
hx-swap="outerHTML">{% if link.needs_review %}🔖{% else %}🏷️{% endif %}</button> hx-swap="outerHTML">{{ icons.bookmark(filled=link.needs_review) }}</button>
<button class="ghost" style="padding:3px 7px; font-size:.8rem;" <button class="ghost icon-btn"
title="Bearbeiten" title="Bearbeiten"
hx-get="/links/{{ link.id }}/edit" hx-get="/links/{{ link.id }}/edit"
hx-target="#link-{{ link.id }}" hx-target="#link-{{ link.id }}"
hx-swap="outerHTML"></button> hx-swap="outerHTML">{{ icons.edit() }}</button>
<button class="danger" style="padding:3px 6px;" <button class="danger icon-btn"
title="Löschen" title="Löschen"
hx-delete="/links/{{ link.id }}" hx-delete="/links/{{ link.id }}"
hx-target="#link-{{ link.id }}" hx-target="#link-{{ link.id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-confirm="Diesen Link wirklich löschen?"></button> hx-confirm="Diesen Link wirklich löschen?">{{ icons.trash() }}</button>
</div> </div>
</div> </div>
@@ -58,6 +58,8 @@
<div class="card-foot"> <div class="card-foot">
<span class="card-domain" title="{{ link.url }}">{{ link.url | domain }}</span> <span class="card-domain" title="{{ link.url }}">{{ link.url | domain }}</span>
<span title="hinzugefügt am {{ link.created_at | datum }}">{{ link.created_at | datum }}</span> <span title="hinzugefügt am {{ link.created_at | datum }}">
{{ icons.clock(size=13) }} {{ link.created_at | datum }}
</span>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
{% import "_icons.html" as icons %}
<div class="panel link-card" id="link-{{ link.id }}"> <div class="panel link-card" id="link-{{ link.id }}">
<form hx-put="/links/{{ link.id }}" hx-target="#link-{{ link.id }}" hx-swap="outerHTML" <form hx-put="/links/{{ link.id }}" hx-target="#link-{{ link.id }}" hx-swap="outerHTML"
hx-disabled-elt="button"> hx-disabled-elt="button">
@@ -5,7 +6,7 @@
{{ link.url }} {{ link.url }}
</div> </div>
<div class="muted" style="font-size:.75rem; margin:3px 0 10px;"> <div class="muted" style="font-size:.75rem; margin:3px 0 10px;">
🕓 hinzugefügt am {{ link.created_at | datum }} {{ icons.clock(size=13) }} hinzugefügt am {{ link.created_at | datum }}
</div> </div>
<div class="field"> <div class="field">
@@ -59,7 +60,8 @@
hx-swap="outerHTML" hx-swap="outerHTML"
hx-disabled-elt="this" hx-disabled-elt="this"
hx-confirm="Link erneut laden und von der KI neu beschreiben lassen? Bestehende Angaben werden dabei ersetzt."> hx-confirm="Link erneut laden und von der KI neu beschreiben lassen? Bestehende Angaben werden dabei ersetzt.">
<span class="htmx-indicator"></span> 🤖 KI neu beschreiben lassen <span class="htmx-indicator icon-spin">{{ icons.spinner(size=14) }}</span>
{{ icons.sparkles(size=14) }} KI neu beschreiben lassen
</button> </button>
{% endif %} {% endif %}
</div> </div>

View File

@@ -128,6 +128,23 @@
.htmx-indicator { opacity: 0; transition: opacity .2s; } .htmx-indicator { opacity: 0; transition: opacity .2s; }
.htmx-request .htmx-indicator { opacity: 1; } .htmx-request .htmx-indicator { opacity: 1; }
.htmx-request.htmx-indicator { opacity: 1; } .htmx-request.htmx-indicator { opacity: 1; }
.icon { display: inline-block; vertical-align: -3px; flex-shrink: 0; }
.icon-spin { animation: icon-spin 0.8s linear infinite; }
@keyframes icon-spin { to { transform: rotate(360deg); } }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0;
}
.icon-warn { color: #eab308; }
.icon-bookmark-active { color: #eab308; }
.input-icon { position: relative; }
.input-icon > .icon {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--muted); pointer-events: none;
}
.input-icon > input { padding-left: 36px; }
footer.app-footer { footer.app-footer {
text-align: center; padding: 16px 24px; margin-top: 24px; text-align: center; padding: 16px 24px; margin-top: 24px;
color: var(--muted); font-size: .8rem; border-top: 1px solid var(--border); color: var(--muted); font-size: .8rem; border-top: 1px solid var(--border);

View File

@@ -1,12 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "_icons.html" as icons %}
{% block title %}Meine Links · LinkVault{% endblock %} {% block title %}Meine Links · LinkVault{% endblock %}
{% block body %} {% block body %}
<header class="topbar"> <header class="topbar">
<div class="brand">Link<span>Vault</span></div> <div class="brand">Link<span>Vault</span></div>
<div class="user-info"> <div class="user-info">
{% if not ai_enabled %} {% if not ai_enabled %}
<span title="Kein OPENAI_API_KEY gesetzt Auto-Kategorisierung deaktiviert"> <span title="Kein OPENAI_API_KEY gesetzt Auto-Kategorisierung deaktiviert" class="icon-warn">
⚠️ KI aus {{ icons.alert(size=14) }} KI aus
</span> </span>
{% endif %} {% endif %}
<span>{{ user.email }}</span> <span>{{ user.email }}</span>
@@ -20,7 +21,7 @@
<aside class="sidebar"> <aside class="sidebar">
<div class="panel"> <div class="panel">
<a href="/?sort={{ sort }}" class="facet {% if not active_category and not active_manufacturer and not review_only %}active{% endif %}"> <a href="/?sort={{ sort }}" class="facet {% if not active_category and not active_manufacturer and not review_only %}active{% endif %}">
<span>📚 Alle Links</span> <span>{{ icons.grid(size=14) }} Alle Links</span>
</a> </a>
<div class="facet-group"> <div class="facet-group">
@@ -50,7 +51,7 @@
<div class="facet-group"> <div class="facet-group">
<h3>Merkliste</h3> <h3>Merkliste</h3>
<a href="/?review=1&amp;sort={{ sort }}" class="facet {% if review_only %}active{% endif %}"> <a href="/?review=1&amp;sort={{ sort }}" class="facet {% if review_only %}active{% endif %}">
<span>🔖 Zur Durchsicht</span><span class="count">{{ facets.review_count }}</span> <span>{{ icons.bookmark(size=14) }} Zur Durchsicht</span><span class="count">{{ facets.review_count }}</span>
</a> </a>
</div> </div>
</div> </div>
@@ -65,7 +66,8 @@
<input type="url" name="url" placeholder="Link einfügen, z.B. https://hersteller.de/produkt" required <input type="url" name="url" placeholder="Link einfügen, z.B. https://hersteller.de/produkt" required
style="flex:1 1 220px; width:auto;"> style="flex:1 1 220px; width:auto;">
<button type="submit" style="white-space:nowrap;"> <button type="submit" style="white-space:nowrap;">
<span class="htmx-indicator"></span> Hinzufügen <span class="htmx-indicator icon-spin">{{ icons.spinner(size=14) }}</span>
{{ icons.plus(size=14) }} Hinzufügen
</button> </button>
</div> </div>
<div class="muted" style="font-size:.8rem; margin-top:6px;"> <div class="muted" style="font-size:.8rem; margin-top:6px;">
@@ -79,11 +81,14 @@
<input type="hidden" name="category" value="{{ active_category }}"> <input type="hidden" name="category" value="{{ active_category }}">
<input type="hidden" name="manufacturer" value="{{ active_manufacturer }}"> <input type="hidden" name="manufacturer" value="{{ active_manufacturer }}">
<input type="hidden" name="review" value="{{ '1' if review_only else '' }}"> <input type="hidden" name="review" value="{{ '1' if review_only else '' }}">
<input type="text" name="q" value="{{ q }}" <div class="input-icon">
placeholder="🔍 Suche in Titel, Zusammenfassung, Tags..." {{ icons.search(size=15) }}
hx-get="/search" hx-target="#links" hx-swap="innerHTML" <input type="text" name="q" value="{{ q }}"
hx-trigger="keyup changed delay:400ms, search" placeholder="Suche in Titel, Zusammenfassung, Tags..."
hx-include="#searchform"> hx-get="/search" hx-target="#links" hx-swap="innerHTML"
hx-trigger="keyup changed delay:400ms, search"
hx-include="#searchform">
</div>
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-top:10px; flex-wrap:wrap;"> <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-top:10px; flex-wrap:wrap;">
{% if ai_enabled %} {% if ai_enabled %}
<label class="muted" style="display:inline-flex; align-items:center; gap:6px; font-size:.85rem;"> <label class="muted" style="display:inline-flex; align-items:center; gap:6px; font-size:.85rem;">