Config-Layout: breiter, zweispaltig, Schalter lesbar, Texte aufklappbar
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user