CTPanelView
ui churchtoolssettings
Präsentation der ChurchTools-Anbindung: Verbindungszustand, Login-Slot und Sync-Fortschritt. Optik geteilt, Engine injiziert — presenter fährt CT-Client + Dexie-Delta-Sync dahinter, worship Broker-Login + resync.
Keine interaktive Live-Demo — siehe Code & Beispiele unten.
Barrierefreiheit
- Der Fortschritt wird als Text mitgeführt, nicht nur als Balken.
Beispiele
Mit App-eigenem Login-Formular
<CTPanelView
connected={loggedIn}
host={host}
loginSlot={<CtLoginForm />}
sync={{ text: "Lieder", counter: "12/40", progress: 0.3 }}
onSync={resync}
/> Genutzte Tokens · 4
--accent--border--muted--status-error
Quelldateien
components/ui/CTPanelView.tsx tsx
import type { ReactNode } from 'react';
import { Button } from './Button.tsx';
import styles from './CTPanelView.module.css';
/**
* Präsentation der ChurchTools-Anbindung (aus media-presenter
* `features/churchtools/ui/CTSettingsPanel.tsx`). Optik geteilt, Engine
* injiziert: das Login-Formular und die Sync-Aktion kommen als Slot/Callback —
* presenter fährt CT-Client + Dexie-Delta-Sync, worship Broker-Login + resync.
*/
export interface SyncView {
text: string;
counter: string;
/** 0..1, oder null für unbestimmten Fortschritt. */
ratio: number | null;
}
export interface CTPanelViewProps {
/** Instanz-/Gemeinde-Bezeichnung für den Untertitel. */
churchName?: string;
loggedIn: boolean;
/** Statuszeile bei loggedIn (z. B. „Angemeldet als …"). */
statusLine?: ReactNode;
/** Login-Bereich, wenn nicht eingeloggt (die App liefert das Formular). */
loginForm?: ReactNode;
onLogout: () => void;
/** Primär-Sync (worship: resync · presenter: Delta-Sync). */
onSync: () => void;
syncBusy: boolean;
syncLabel?: string;
progress?: SyncView | null;
lastSyncAt?: number | null;
error?: string | null;
/** App-spezifische Zusatz-Aktionen (presenter: Voll-Sync, Abgleich). */
actions?: ReactNode;
summary?: ReactNode;
}
export function CTPanelView({
churchName,
loggedIn,
statusLine,
loginForm,
onLogout,
onSync,
syncBusy,
syncLabel = 'Jetzt synchronisieren',
progress,
lastSyncAt,
error,
actions,
summary,
}: CTPanelViewProps) {
return (
<div className={styles.wrap}>
<header>
<h2 className={styles.title}>ChurchTools-Anbindung</h2>
{churchName && (
<p className={styles.subtitle}>{churchName} — Lieder und Gottesdienste aus ChurchTools.</p>
)}
</header>
<section className={styles.section}>
{!loggedIn ? (
loginForm
) : (
<>
{statusLine && <p className={styles.status}>{statusLine}</p>}
<div className={styles.actions}>
<Button variant="primary" onClick={onSync} disabled={syncBusy}>
{syncBusy ? 'Synchronisiere…' : syncLabel}
</Button>
{actions}
<Button variant="ghost" onClick={onLogout} disabled={syncBusy}>
Abmelden
</Button>
</div>
{progress && (
<div className={styles.progress}>
<div className={styles.progressLine}>
<span className={styles.progressLabel}>{progress.text}</span>
{progress.counter && <span className={styles.progressCount}>{progress.counter}</span>}
</div>
<div className={styles.progressBar}>
{progress.ratio == null ? (
<div className={styles.progressFillIndeterminate} />
) : (
<div className={styles.progressFill} style={{ transform: `scaleX(${progress.ratio})` }} />
)}
</div>
</div>
)}
{lastSyncAt && !syncBusy && (
<p className={styles.status}>
Zuletzt synchronisiert: {new Date(lastSyncAt).toLocaleString('de-DE')}
</p>
)}
</>
)}
{error && <p className={styles.error}>{error}</p>}
{summary}
</section>
</div>
);
}
components/ui/CTPanelView.module.css css
.wrap {
display: flex;
flex-direction: column;
gap: 1.25rem;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.title {
margin: 0 0 0.25rem 0;
font-size: 1.125rem;
font-weight: 600;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: 0.875rem;
}
.section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.status {
font-size: 0.875rem;
color: var(--muted);
}
.error {
color: var(--status-error);
font-size: 0.875rem;
white-space: pre-wrap;
}
.progress {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.875rem;
}
.progressLine {
display: flex;
justify-content: space-between;
gap: 0.5rem;
color: var(--muted);
}
.progressLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.progressCount {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.progressBar {
position: relative;
height: 6px;
background: var(--border);
border-radius: 999px;
overflow: hidden;
}
.progressFill {
position: absolute;
inset: 0;
background: var(--accent);
transform-origin: left center;
transition: transform 120ms linear;
}
.progressFillIndeterminate {
position: absolute;
top: 0;
left: 0;
width: 30%;
height: 100%;
background: var(--accent);
border-radius: 999px;
animation: ct-indeterminate 1.1s ease-in-out infinite;
}
@keyframes ct-indeterminate {
0% {
left: -30%;
}
100% {
left: 100%;
}
}
Holen
# MCP (Claude Code / Cursor)
get_component({ name: "ct-panel-view" })
# Registry direkt
https://www.ekklesi.tools/design/registry/components/ct-panel-view.json