M23: Experte-Tab-Sektionsüberschriften prominenter + einklappbar

Section-Header größer/fett mit adaptivem accentColor-Hintergrund statt
macOS' winzigem Kapitälchen-Default. Einklappbar per eigenem Chevron
(immer sichtbar, nicht macOS' Hover-only-Sidebar-Dreieck) statt
Section(isExpanded:), da SwiftUI dessen natives Disclosure-Verhalten
nicht anpassen lässt. Startet standardmäßig komplett zugeklappt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kay
2026-09-16 11:03:39 +02:00
co-authored by Claude Sonnet 5
parent a0e7e53f22
commit 567feed93a
3 changed files with 114 additions and 27 deletions
+29
View File
@@ -1389,6 +1389,35 @@ in eine private `resetToInitialState()` plus zwei benannte Wrapper
Aufrufkontext (erfolgreicher Apply vs. Nutzer bricht ab) unter dem
passenden Namen lesbar. Live bestätigt ("sieht gut aus").
**M23: Experte-Tab-Sektionsüberschriften prominenter + einklappbar**
(2026-09-16, Nutzerwunsch: "die Sektionsüberschriften ... müssen
prominenter sein und eingefärbt, bitte größer machen und passend
farblich hinterlegen (darkmode beachten)", danach "die einzelnen
Sektion einklappbar machen"). Zwei Schritte:
1. `Section("...")`s Standard-Header (macOS' winzige graue Kapitälchen-
Beschriftung) durch eigenen `Text` ersetzt: größer/fett
(`.title3.bold()`), Hintergrund `Color.accentColor.opacity(0.18)`
statt eines festen RGB-Werts, damit Light/Dark Mode automatisch
passend kontrastieren — `.textCase(nil)` nötig, sonst erzwingt macOS
trotzdem wieder seine eigene Kapitälchen-Darstellung über dem
eigenen View.
2. Einklappbar gemacht — erster Versuch mit SwiftUIs eingebautem
`Section(isExpanded:)` (native Disclosure-Dreieck), vom Nutzer
sofort korrigiert: "der Pfeil sollte immer zu sehen sein und nicht
erst beim Hovern" — das native macOS-Sidebar-Dreieck ist genau wie
in Finder standardmäßig Hover-only, keine SwiftUI-Option dafür
vorgesehen. Fix: eigenes Chevron-Icon (`chevron.right`, per
`.rotationEffect` gedreht) direkt im selbstgebauten Header, ganzer
Header als `Button` klickbar; `Section(isExpanded:)`-API komplett
fallengelassen zugunsten eines `if isExpanded { ForEach(...) }`
innerhalb eines schlichten `Section { }`, gesteuert über eigenen
`@State`/Binding (`collapsedCategories: Set<RouterOSMenuCategory>`
für die neun Kategorien, `isCustomPathExpanded: Bool` für "Eigener
Menüpfad"). Auf Nutzerwunsch ("standardmäßig sollten beim Öffnen des
Experte-Tab alle Sektion zugeklappt sein") startet alles zugeklappt.
`.listStyle(.sidebar)` an der `List` ergänzt, für den nativen
macOS-Sidebar-Look. Live bestätigt ("sehr gut, bin sehr zufrieden").
## Nächste Schritte
1. ~~M16: restliche `RouterOSSchemaCatalog.swift`-Sektionen übersetzen~~,
+2 -1
View File
@@ -151,7 +151,8 @@ nur die zugehörigen Passwörter liegen weiterhin im macOS-Schlüsselbund.
| M19 | Übersicht-Tab: Flussanimation + verschiebbare Knoten | ✅ live verifiziert |
| M20 | LAN-Scanner: Umbenennung, Netzwerk-Tools (Ping/Traceroute/DNS/Port-Scan) | ✅ live verifiziert |
| M21 | Zweisprachigkeit (DE/EN) auf alle fünf Tabs ausgerollt (Einrichten/Übersicht/LAN-Scanner/Sicherungen) | ✅ live verifiziert |
| M22 | REST-Transport (M7) erstmals live gegen Hardware verifiziert, 3 Bugs gefunden+gefixt | ✅ live verifiziert |
| M22 | REST-Transport (M7) erstmals live gegen Hardware verifiziert, 4 Bugs gefunden+gefixt | ✅ live verifiziert |
| M23 | Experte-Tab: Sektionsüberschriften prominenter+eingefärbt, einklappbar (Standard: zugeklappt) | ✅ live verifiziert |
| — | LAN-Port-Konflikt-Prüfung + "Fertig"-Button (Einrichten) | 🔶 gebaut, Live-Test offen |
Ausführlicher Stand inkl. aller gefundenen Bugs, offener Punkte und
@@ -4,6 +4,11 @@ struct ExpertView: View {
@ObservedObject var connectionService: ConnectionService
@StateObject private var viewModel: ExpertViewModel
@AppStorage("appLanguage") private var appLanguage: String = "de"
/// A category's presence in this set means "collapsed". Starts with every category
/// collapsed (Nutzerwunsch: "standardmäßig sollten beim Öffnen des Experte-Tab alle
/// Sektion zugeklappt sein") click a header to fold its menu list open again.
@State private var collapsedCategories: Set<RouterOSMenuCategory> = Set(RouterOSMenuCategory.allCases)
@State private var isCustomPathExpanded = false
init(connectionService: ConnectionService) {
self.connectionService = connectionService
@@ -24,40 +29,58 @@ struct ExpertView: View {
ForEach(RouterOSMenuCategory.allCases) { category in
let schemas = RouterOSSchemaCatalog.schemas(in: category)
if !schemas.isEmpty {
Section(LocalizedStringKey(L10n.t(category.rawValue, appLanguage))) {
ForEach(schemas) { schema in
let isSelected = viewModel.selectedSchema?.menuPath == schema.menuPath
Button {
viewModel.open(schema)
} label: {
Text(L10n.t(schema.displayName, appLanguage))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(isSelected ? Color.blue.opacity(0.25) : Color.clear)
)
.contentShape(Rectangle())
let isExpanded = !collapsedCategories.contains(category)
Section {
if isExpanded {
ForEach(schemas) { schema in
let isSelected = viewModel.selectedSchema?.menuPath == schema.menuPath
Button {
viewModel.open(schema)
} label: {
Text(L10n.t(schema.displayName, appLanguage))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(isSelected ? Color.blue.opacity(0.25) : Color.clear)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
} header: {
sectionHeader(L10n.t(category.rawValue, appLanguage), isExpanded: isExpanded) {
if isExpanded {
collapsedCategories.insert(category)
} else {
collapsedCategories.remove(category)
}
.buttonStyle(.plain)
}
}
}
}
Section(LocalizedStringKey(L10n.t("Eigener Menüpfad", appLanguage))) {
Text(L10n.t("Jeder RouterOS-Menüpfad ist erreichbar, auch wenn er oben nicht gelistet ist — die Felder erscheinen dann generisch (Schlüssel/Wert), ohne kuratierte Erklärung.", appLanguage))
.font(.caption)
.foregroundStyle(.secondary)
TextField(L10n.t("Menüpfad, z.B. /ip firewall filter", appLanguage), text: $viewModel.customMenuPath)
.help(L10n.t("Der RouterOS-CLI-Pfad, z.B. \"/interface wireguard\" oder \"/routing ospf instance\".", appLanguage))
TextField(L10n.t("REST-Pfad, z.B. ip/firewall/filter", appLanguage), text: $viewModel.customRestPath)
.help(L10n.t("Derselbe Pfad in REST-API-Schreibweise: führende \"/\" weg, Leerzeichen zu \"/\".", appLanguage))
Button(L10n.t("Öffnen", appLanguage)) { viewModel.openCustomPath() }
.disabled(viewModel.customMenuPath.isEmpty || viewModel.customRestPath.isEmpty)
Section {
if isCustomPathExpanded {
Text(L10n.t("Jeder RouterOS-Menüpfad ist erreichbar, auch wenn er oben nicht gelistet ist — die Felder erscheinen dann generisch (Schlüssel/Wert), ohne kuratierte Erklärung.", appLanguage))
.font(.caption)
.foregroundStyle(.secondary)
TextField(L10n.t("Menüpfad, z.B. /ip firewall filter", appLanguage), text: $viewModel.customMenuPath)
.help(L10n.t("Der RouterOS-CLI-Pfad, z.B. \"/interface wireguard\" oder \"/routing ospf instance\".", appLanguage))
TextField(L10n.t("REST-Pfad, z.B. ip/firewall/filter", appLanguage), text: $viewModel.customRestPath)
.help(L10n.t("Derselbe Pfad in REST-API-Schreibweise: führende \"/\" weg, Leerzeichen zu \"/\".", appLanguage))
Button(L10n.t("Öffnen", appLanguage)) { viewModel.openCustomPath() }
.disabled(viewModel.customMenuPath.isEmpty || viewModel.customRestPath.isEmpty)
}
} header: {
sectionHeader(L10n.t("Eigener Menüpfad", appLanguage), isExpanded: isCustomPathExpanded) {
isCustomPathExpanded.toggle()
}
}
}
.listStyle(.sidebar)
.navigationTitle(LocalizedStringKey(L10n.t("Experte", appLanguage)))
} detail: {
if let schema = viewModel.selectedSchema {
@@ -76,6 +99,40 @@ struct ExpertView: View {
}
}
}
/// Custom Section header plain `Section("title")` rendered as macOS' default tiny
/// gray-caps sidebar label, too easy to miss when scanning nine menu categories (Nutzerwunsch:
/// "müssen prominenter sein und eingefärbt"). `.textCase(nil)` is required here, or macOS
/// forces its own uppercase-small-caps styling back on top of this custom view regardless of
/// the font/background set below. `Color.accentColor` (not a fixed RGB value) so the tint
/// stays visible and correctly contrasted in both light and dark mode automatically.
///
/// The chevron is drawn explicitly here rather than relying on `Section(isExpanded:)`'s
/// built-in disclosure triangle that one is macOS sidebar-standard hover-only (matches
/// Finder), which the user explicitly didn't want ("der Pfeil sollte immer zu sehen sein und
/// nicht erst beim Hovern"). The whole header is a plain `Button` toggling `onToggle`, with
/// the section's own content shown/hidden by the caller via a plain `if isExpanded` instead
/// of the `Section(isExpanded:)` API, since that API owns its own (hover-only) chevron.
private func sectionHeader(_ title: String, isExpanded: Bool, onToggle: @escaping () -> Void) -> some View {
Button(action: onToggle) {
HStack(spacing: 6) {
Image(systemName: "chevron.right")
.font(.caption.bold())
.foregroundStyle(.secondary)
.rotationEffect(.degrees(isExpanded ? 90 : 0))
Text(title)
.font(.title3.bold())
.foregroundStyle(.primary)
}
.padding(.vertical, 6)
.padding(.horizontal, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 6).fill(Color.accentColor.opacity(0.18)))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.textCase(nil)
}
}
#Preview {