AppWindowHeader
ui layoutheaderapp-window
Kopfzeile des App-Fensters im Raster auto·1fr·auto: Marke links, Suchschlitz mittig, Status und Konto rechts. Die Slots sind offen — Logo, Badges und Profilmenü sind Inhalte (SearchInput, LicenseBadge, AccountMenu), nicht Aufgabe des Fensters. Mit stackSearchWhenNarrow rutscht die Suche unter 1180px in eine eigene Zeile (opt-in).
ekklesi:demo
Pro
Abhängigkeiten
Barrierefreiheit
- Bringt selbst keine Landmark mit — sie kommt vom <header> des AppWindow. Bei eigenständiger Verwendung selbst eine setzen.
Beispiele
Vollständige Kopfzeile
<AppWindowHeader
brand={<Brand />}
search={<SearchInput value={q} onChange={setQ} />}
status={<><CtBadge /><LicenseBadge plan={plan} /></>}
account={<AccountMenu … />}
/> stackSearchWhenNarrow — nur bei voller Kopfzeile
// OPT-IN: der Umbruch hängt am INHALT, nicht an der Fensterbreite.
// presenter trägt drei Pillen neben einer 480px-Suche und braucht ihn:
<AppWindowHeader stackSearchWhenNarrow brand={…} search={…} status={…} account={…} />
// worship hat nur eine Badge — ohne die Prop bleibt die Kopfzeile
// einzeilig (57px statt 109px):
<AppWindowHeader brand={…} search={…} status={<QuellenBadge />} account={…} /> Genutzte Tokens · 4
--app-window-gutter--app-window-header-height--space-3--space-5
Quelldateien
components/ui/AppWindowHeader.tsx tsx
import type { ReactNode } from 'react';
import styles from './AppWindowHeader.module.css';
/**
* Kopfzeile des App-Fensters: Marke links, Suchschlitz mittig, Status und
* Konto rechts.
*
* Raster `auto 1fr auto` nach dem Vorbild aus worship (`WorshipHeader`) — der
* presenter nutzte bisher `flex` mit `space-between`, wodurch die Mitte nur
* zufällig mittig lag. Die Slots sind bewusst offen: Logo, Badges und
* Profilmenü sind INHALTE (`AccountMenu`, `LicenseBadge`, `SearchInput`),
* nicht Aufgabe des Fensters.
*/
export interface AppWindowHeaderProps {
/** Links: Signet + Wortmarke + Version. */
brand: ReactNode;
/** Mitte: Suchschlitz. Fehlt sie, bleibt die Spur leer und schiebt den
* rechten Block ans Ende. */
search?: ReactNode;
/** Rechts vor dem Konto: CT-Badge, Lizenz-Badge, App-Extras. */
status?: ReactNode;
/** Ganz rechts: Profil-Icon / Konto-Menü. */
account?: ReactNode;
/**
* Unter 1180px die Suche in eine eigene Zeile umbrechen (Kopfzeile wird
* zweizeilig). OPT-IN, weil es vom Inhalt abhängt, nicht von der Breite:
* der presenter trägt rechts drei Pillen neben einer 480px-Suche und braucht
* den Umbruch; worship kommt mit einer Badge in einer Zeile aus und würde
* sonst 53px Höhe verschenken.
*/
stackSearchWhenNarrow?: boolean;
className?: string;
}
export function AppWindowHeader({
brand,
search,
status,
account,
stackSearchWhenNarrow = false,
className,
}: AppWindowHeaderProps) {
const cls = [styles.header, stackSearchWhenNarrow ? styles.stack : '', className]
.filter(Boolean)
.join(' ');
return (
<div className={cls}>
<div className={styles.brand}>{brand}</div>
<div className={styles.search}>{search}</div>
<div className={styles.right}>
{status}
{account}
</div>
</div>
);
}
components/ui/AppWindowHeader.module.css css
.header {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-5);
min-height: var(--app-window-header-height);
padding-inline: var(--app-window-gutter);
}
.brand {
display: flex;
align-items: center;
min-width: 0;
}
.search {
display: flex;
justify-content: center;
min-width: 0;
}
.right {
display: flex;
align-items: center;
gap: var(--space-3);
justify-self: end;
}
/* Schmale Fenster: die Suche rutscht in eine eigene Zeile unter Marke und
Konto. Der presenter löste das früher über `> :nth-child(2)` — das brach,
sobald die Slots gerastert wurden. Hier hängt es an der Klasse. */
@media (max-width: 1180px) {
.header.stack {
grid-template-columns: auto 1fr;
grid-template-areas:
'brand right'
'search search';
row-gap: var(--space-5);
padding-block: var(--space-3);
}
.stack .brand {
grid-area: brand;
}
.stack .right {
grid-area: right;
}
.stack .search {
grid-area: search;
}
/* Die Suche soll die volle Zeile füllen. `justify-content: stretch` wirkt im
Flex-Container nicht — das Kind muss selbst wachsen dürfen, und sein
`max-width` (im presenter 480px) muss weg.
`.header .search > *` statt `.search > *`: bei gleicher Spezifität gewinnt
die zuletzt eingebundene Datei, und die App-CSS steht nach der des
Pakets. Zwei Klassen schlagen die eine der Konsumenten-Regel. */
.header.stack .search > * {
flex: 1;
width: 100%;
max-width: none;
}
}
Holen
# MCP (Claude Code / Cursor)
get_component({ name: "app-window-header" })
# Registry direkt
https://www.ekklesi.tools/design/registry/components/app-window-header.json