forked from kay/RouterOS
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:
+29
@@ -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~~,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user