StatusPill

ui statusshellheader

Status-Pille der Kopfzeile (Lizenz/CT): Mono-Label mit Ampel-Punkt, als Trigger für ein DropdownMenu. Feste 32px-Höhe wie der AccountMenu-Trigger — beide stehen in derselben Header-Zeile. `StatusDot` gibt es einzeln für die Popover-Überschrift, damit Pille und Detailansicht dieselbe Ampel zeigen.

Punkt einzeln (Popover-Überschrift)

Abhängigkeiten

Barrierefreiheit

  • Trigger trägt aria-haspopup="menu" und spiegelt den Popover-Zustand in aria-expanded.
  • Die Ampel ist nie die einzige Information — aria-label benennt den Zustand in Worten.

Beispiele

Als Dropdown-Trigger
<DropdownMenu
  ariaLabel="Lizenzstatus"
  trigger={({ open, toggle }) => (
    <StatusPill ok={isPro} label="Lizenz" open={open} onToggle={toggle}
      ariaLabel={isPro ? "Lizenzstatus: Pro aktiv" : "Lizenzstatus: Basis"} />
  )}
>
  …
</DropdownMenu>

Genutzte Tokens · 13

--bg--border--danger--ease-standard--fg--focus-ring--font-mono--motion-fast--muted--radius-pill--space-2--space-3--success

Quelldateien

components/ui/StatusPill.tsx tsx
import styles from './StatusPill.module.css';

/**
 * Status-Pille der Kopfzeile (Lizenz/CT): Mono-Label in Versalien mit
 * Ampel-Punkt, als Trigger für ein `DropdownMenu` gedacht. Store-frei
 * (Softkey-Vertrag): Zustand und Popover-Inhalt verdrahtet die App.
 *
 * Lebte vorher als Kopie in presenter UND worship (`StatusIndicators`), mit
 * eigener Höhe (~26px aus Padding) neben dem 32px-`AccountMenu`-Trigger —
 * in derselben Header-Zeile las sich der Unterschied als Versehen. Jetzt
 * teilen sich beide die feste Höhe und die Hover-/Focus-Sprache.
 */
export interface StatusPillProps {
  /** Ampel: grün = in Ordnung, rot = Handlungsbedarf. */
  ok: boolean;
  label: string;
  /** Popover offen? Hält den Hover-Zustand, solange das Menü offen ist. */
  open?: boolean;
  onToggle: () => void;
  ariaLabel: string;
}

export function StatusPill({ ok, label, open = false, onToggle, ariaLabel }: StatusPillProps) {
  return (
    <button
      type="button"
      className={`${styles.pill} ${open ? styles.pillOpen : ''}`}
      aria-haspopup="menu"
      aria-expanded={open}
      aria-label={ariaLabel}
      onClick={onToggle}
    >
      <StatusDot ok={ok} />
      {label}
    </button>
  );
}

/** Derselbe Ampel-Punkt für die Popover-Überschrift — Pille und Detailansicht
 *  sollen dieselbe Farbe aus derselben Quelle zeigen. */
export function StatusDot({ ok }: { ok: boolean }) {
  return <span className={`${styles.dot} ${ok ? styles.dotOk : styles.dotBad}`} aria-hidden="true" />;
}
components/ui/StatusPill.module.css css
/* Feste 32px wie der AccountMenu-Trigger: beide stehen in derselben
   Header-Zeile, und die alte inhaltsgetriebene Höhe (~26px aus 6px-Padding)
   ließ die Pillen neben dem Profil-Button geschrumpft wirken. Border, Radius,
   Hover und Focus-Ring sind bewusst identisch zum Trigger — die Mono-Schrift
   in Versalien bleibt das Erkennungszeichen der Status-Sprache. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 32px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}
.pill:hover,
.pillOpen {
  border-color: color-mix(in oklab, var(--border), var(--fg) 25%);
  color: var(--fg);
}
.pill:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
}
.dotOk {
  background: var(--success);
}
.dotBad {
  background: var(--danger);
}

Holen

# MCP (Claude Code / Cursor)
get_component({ name: "status-pill" })

# Registry direkt
https://www.ekklesi.tools/design/registry/components/status-pill.json