feat(approval-pill): icon-only eye pill on /deadlines + /appointments + /agenda

m's 2026-05-08 cosmetic ask: the "Wartet auf Genehmigung" badge ate
row width and read as a noisy block of text on every pending row.
Replace with a 22px eye-icon pill; the lifecycle label moves to the
hover tooltip (title attr + aria-label so screen readers still get
the full text).

Three pieces:

  - global.css — new .approval-pill--icon modifier sets the pill to
    a circular 22×22 hit target with a centered SVG. Base
    .approval-pill (text-pill behavior) and --historic (inbox status
    pill) stay untouched so the inbox surface keeps rendering the
    full status + decider name.
  - client/events.ts (the /deadlines + /appointments shell) and
    client/agenda.ts each get a tiny APPROVAL_PILL_EYE_SVG constant
    + the new --icon class on the pending pill. Two definitions
    (no shared icons module today; no other surfaces need this glyph
    yet) — the duplication is two lines, easier to read than yet
    another import.

What it looks like: 👁 in a soft amber circle, hovers to "Änderung
wartet auf Genehmigung" / "Erledigung wartet auf Genehmigung" / etc.
The lifecycle-specific label kept (no schema work) — Maria gated this
slice as pure-frontend; the richer "wartet auf Genehmigung von
<role>; angefragt am <date>" tooltip needs a backend join we're not
doing here.

Refs t-paliad-160 §C / m's 2026-05-08 18:15 batch Item B.
This commit is contained in:
m
2026-05-08 18:18:16 +02:00
parent ef78f59d25
commit 4bab520119
3 changed files with 47 additions and 5 deletions

View File

@@ -2,6 +2,13 @@ import { initI18n, onLangChange, t, tDyn, getLang } from "./i18n";
import { initSidebar } from "./sidebar";
import { attachEventTypeMultiSelectFilter, type FilterHandle } from "./event-types";
// Eye-icon SVG used inside .approval-pill--icon (mirrors events.ts).
const APPROVAL_PILL_EYE_SVG =
'<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z"/>' +
'<circle cx="12" cy="12" r="3"/>' +
'</svg>';
let eventTypeFilter: FilterHandle | null = null;
type Urgency = "overdue" | "today" | "tomorrow" | "this_week" | "later";
@@ -279,8 +286,11 @@ function renderItem(it: AgendaItem, bucketUrgency: Urgency): string {
const project = it.project_id
? `<a class="agenda-item-project" href="/projects/${esc(it.project_id)}">${esc(formatProjectLabel(it))}</a>`
: "";
// Icon-only pill — eye glyph + tooltip with the lifecycle label.
// m's 2026-05-08 cosmetic ask.
const pendingLabel = it.approval_status === "pending" ? tDyn("approvals.pending_update.label") : "";
const pendingPill = it.approval_status === "pending"
? `<span class="approval-pill" title="${esc(tDyn("approvals.pending_update.label"))}">${esc(tDyn("approvals.pending_update.label"))}</span>`
? `<span class="approval-pill approval-pill--icon" title="${esc(pendingLabel)}" aria-label="${esc(pendingLabel)}">${APPROVAL_PILL_EYE_SVG}</span>`
: "";
const timePart = it.type === "appointment"

View File

@@ -9,6 +9,16 @@ import {
} from "./event-types";
import { projectIndent } from "./project-indent";
// Eye-icon SVG used inside .approval-pill--icon. Kept as a string
// constant rather than a separate module since only events.ts and
// agenda.ts render it; the duplication is two lines, easier to read
// than yet another import.
const APPROVAL_PILL_EYE_SVG =
'<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z"/>' +
'<circle cx="12" cy="12" r="3"/>' +
'</svg>';
// EventsPage shared client (t-paliad-110). Drives /deadlines and
// /appointments off the same shell — the route handler injects
// `window.__PALIAD_EVENTS__ = { defaultType: "deadline" | "appointment" }`
@@ -507,12 +517,13 @@ function renderRow(item: EventListItem, showReopen: boolean): string {
? `<span class="termin-type-chip termin-type-${esc(item.appointment_type)}">${esc(tDyn(`appointments.type.${item.appointment_type}`) || item.appointment_type)}</span>`
: "&mdash;";
// Approval pending pill (t-paliad-138). Soft-tint the row + insert a
// ⚠ chip next to the title. Generic "pending approval" — the inbox
// shows the lifecycle detail.
// Approval pending pill (t-paliad-138 / m's 2026-05-08 cosmetic ask).
// Soft-tint the row + drop an eye-icon pill next to the title; hover
// reveals the lifecycle label. Inbox surface shows the full detail.
const pendingClass = item.approval_status === "pending" ? " entity-row--pending-update" : "";
const pendingLabel = item.approval_status === "pending" ? t("approvals.pending_update.label") : "";
const pendingPill = item.approval_status === "pending"
? `<span class="approval-pill" title="${esc(t("approvals.pending_update.label"))}">${esc(t("approvals.pending_update.label"))}</span>`
? `<span class="approval-pill approval-pill--icon" title="${esc(pendingLabel)}" aria-label="${esc(pendingLabel)}">${APPROVAL_PILL_EYE_SVG}</span>`
: "";
return `<tr class="frist-row events-row events-row-${item.type}${pendingClass}" data-id="${esc(item.id)}" data-type="${item.type}">

View File

@@ -10594,6 +10594,27 @@ dialog.quick-add-sheet::backdrop {
}
.approval-pill::before { content: "⚠"; }
/* Icon-only variant — eye glyph signals "preliminary, awaiting approval"
* without consuming row width. Hover (title attr) reveals the full
* lifecycle label. m's 2026-05-08 ask: smaller pill, eye icon, tooltip. */
.approval-pill--icon {
width: 22px;
height: 22px;
padding: 0;
justify-content: center;
cursor: help;
}
.approval-pill--icon::before { content: ""; }
.approval-pill--icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.approval-pill--historic {
background: var(--bg-soft);
color: var(--fg-muted);