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.

Abhängigkeiten

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