Config-Layout: breiter, zweispaltig, Schalter lesbar, Texte aufklappbar
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Drei Sachen, die die Seite erschlagend gemacht haben.

Die Schalter klebten am Text, weil `.field > label` spezifischer ist als
`.toggle` — die Regel hat den Schalter zu einem Block in Versalien gemacht
und Beschriftung plus Hinweis in eine Zeile gequetscht. Jetzt steht der
Schalter links, Beschriftung und Hinweis darüber/darunter, in normaler
Schrift. Der Schalter ist außerdem so groß wie der auf den Modul-Karten.

Der Inhalt war auf 1060px begrenzt, obwohl hier Formulare stehen und keine
Fließtexte. Jetzt 1440px, und die reinen Formular-Tabs (Feeds, Community,
Support, System) stellen ihre Blöcke ab 1180px nebeneinander. Feeds ist
damit 1448 statt 2453 Pixel hoch. Tabs mit Editor oder Vorschau —
Composer, Seiten, Server, Brand — bleiben einspaltig.

Der Texte-Tab zeigte 27 offene Textfelder gleichzeitig. Jetzt ist es eine
Liste zum Aufklappen: pro Zeile Name und erste Zeile des Texts, geöffnet
wird eine nach der anderen. Ungespeicherte Änderungen sind an der Marke
und einem Balken am Rand erkennbar.

Nebenbei: eine geänderte Adresse (#feeds) wechselte den Bereich nicht,
weil niemand auf hashchange gehört hat. Aufgefallen ist das erst, weil ein
Test durch reine Hash-Navigation stillschweigend auf dem alten Tab blieb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
D4rkst3randClaude Fable 5 committed 2026-07-31 16:33:09 +02:00
1 parent a32c267dda
commit 90361fa55f
2 files changed
+185 -70

No files matched your search

+80 -46
View File
@@ -67,6 +67,10 @@ const TABS = [
find: 'mitarbeiter rechte bereiche protokoll audit zugriff' },
];
// Tabs, die nur aus Formular-Blöcken bestehen — die dürfen zweispaltig laufen.
// Alles mit Editor, Vorschau oder Liste bleibt einspaltig.
const DENSE_TABS = new Set(['feeds', 'community', 'support', 'system']);
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
const TAB_SCOPES = {
status: 'any',
@@ -222,6 +226,7 @@ export default function Settings({ me }) {
const [templates, setTemplates] = useState([]);
const [templateGroups, setTemplateGroups] = useState([]);
const [tplDraft, setTplDraft] = useState({}); // id → bearbeiteter Text
const [openTpl, setOpenTpl] = useState(null); // aufgeklappte Vorlage
// Eigene Seiten (Regeln, Über uns, …)
const emptyPage = { slug: '', title: '', content: '', published: false, in_menu: true, sort: 0, isNew: true };
const [pages, setPages] = useState([]);
@@ -300,6 +305,16 @@ export default function Settings({ me }) {
if (data?.status?.botDescription != null) setBotDesc(data.status.botDescription);
}, [data]);
// Zurück-Taste und von Hand geänderte Adressen sollen den Bereich mitwechseln
useEffect(() => {
const onHash = () => {
const id = TABS.find((t) => t.id === window.location.hash.slice(1))?.id;
if (id) setTabState(id);
};
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
// Falls der aktive Tab nicht (mehr) erlaubt ist → auf den ersten erlaubten wechseln
useEffect(() => {
if (!canTab(tab) && visibleTabs.length > 0) setTab(visibleTabs[0].id);
@@ -1890,10 +1905,10 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Texte</h2>
<p className="section-intro">
Alles, was der Bot nach außen schreibt — Begrüßungen, Bestätigungen,
Direktnachrichten. Die Bausteine in geschweiften Klammern setzt der Bot
beim Senden ein; klick einen an, um ihn an der Cursor-Position einzufügen.
Ein leeres Feld bedeutet: wieder der Standardtext.
Alles, was der Bot nach außen schreibt. Zeile anklicken zum Bearbeiten —
die Bausteine in geschweiften Klammern setzt der Bot beim Senden ein,
ein Klick darauf fügt sie an der Cursor-Position ein. Ein leeres Feld
bedeutet: wieder der Standardtext.
</p>
{templateGroups.map((group) => {
@@ -1910,56 +1925,75 @@ export default function Settings({ me }) {
const value = draft ?? t.text;
const dirty = draft != null && draft !== t.text;
const off = t.module && modules.some((m) => m.id === t.module && !m.enabled);
const open = openTpl === t.id;
return (
<div className={`card tpl-card${t.long ? ' long' : ''}${dirty ? ' dirty' : ''}`} key={t.id}>
<div className="tpl-head">
<span className="tpl-name">{t.name}</span>
{t.customized && <span className="tpl-badge">angepasst</span>}
<div
className={`tpl-row${open ? ' open' : ''}${dirty ? ' dirty' : ''}`}
key={t.id}
>
<button
className="tpl-head"
aria-expanded={open}
onClick={() => setOpenTpl(open ? null : t.id)}
>
<span className="tpl-head-main">
<span className="tpl-name">{t.name}</span>
{/* Zusammengeklappt reicht die erste Zeile als Erkennungszeichen */}
<span className="tpl-peek">{value.split('\n')[0]}</span>
</span>
{dirty && <span className="tpl-badge">ungespeichert</span>}
{!dirty && t.customized && <span className="tpl-badge">angepasst</span>}
{off && <span className="tpl-badge muted">Modul aus</span>}
</div>
<IconChevronDown className={`tpl-caret${open ? ' up' : ''}`} size={15} />
</button>
<textarea
id={`tpl-${t.id}`}
className="tpl-input"
rows={t.long ? 4 : 2}
value={value}
spellCheck={false}
onChange={(e) => setTplDraft((old) => ({ ...old, [t.id]: e.target.value }))}
/>
{open && (
<div className="tpl-body">
<textarea
id={`tpl-${t.id}`}
className="tpl-input"
rows={t.long ? 5 : 2}
value={value}
spellCheck={false}
onChange={(e) => setTplDraft((old) => ({ ...old, [t.id]: e.target.value }))}
/>
{t.vars.length > 0 && (
<div className="tpl-vars">
{t.vars.map((v) => (
{t.vars.length > 0 && (
<div className="tpl-vars">
<span className="tpl-vars-label">Bausteine:</span>
{t.vars.map((v) => (
<button
key={v.key}
className="tpl-var"
title={`${v.desc} — einfügen`}
onClick={() => insertPlaceholder(t, v.key)}
>
{`{${v.key}}`}
</button>
))}
</div>
)}
<div className="tpl-actions">
<button
key={v.key}
className="tpl-var"
title={v.desc}
onClick={() => insertPlaceholder(t, v.key)}
className="btn"
disabled={!dirty}
onClick={() => saveTemplate(t.id, value)}
>
{`{${v.key}}`}
Speichern
</button>
))}
{(t.customized || dirty) && (
<button
className="btn btn-ghost with-icon"
onClick={() => saveTemplate(t.id, '')}
title="Standardtext wiederherstellen"
>
<IconRefresh size={13} /> Standard
</button>
)}
</div>
</div>
)}
<div className="tpl-actions">
<button
className="btn"
disabled={!dirty}
onClick={() => saveTemplate(t.id, value)}
>
Speichern
</button>
{(t.customized || dirty) && (
<button
className="btn btn-ghost with-icon"
onClick={() => saveTemplate(t.id, '')}
title="Standardtext wiederherstellen"
>
<IconRefresh size={13} /> Standard
</button>
)}
</div>
</div>
);
})}
@@ -2229,7 +2263,7 @@ export default function Settings({ me }) {
})}
</nav>
<div className="settings-content">
<div className={`settings-content${DENSE_TABS.has(tab) ? ' dense' : ''}`}>
{content}
{showSave && (
<div className="settings-actions">