diff --git a/Outpost/Features/Account/SettingsSidebarList.swift b/Outpost/Features/Account/SettingsSidebarList.swift index bb0f99c..85054de 100644 --- a/Outpost/Features/Account/SettingsSidebarList.swift +++ b/Outpost/Features/Account/SettingsSidebarList.swift @@ -6,6 +6,10 @@ import SwiftUI /// content, rather than a separate mini sidebar nested inside a page. "Done" /// clears `AppNavigation.isShowingSettings`, which puts the collections tree /// back. +/// +/// Grouped by `SettingsCategory` — `general` (ours) sits unlabeled at the +/// top, then Outline's own Account/Workspace/Integrations & Installation +/// groups, matching the settings page structure of the Outline web app. struct SettingsSidebarList: View { @Binding var selection: SettingsSection? let onDone: () -> Void @@ -22,9 +26,20 @@ struct SettingsSidebarList: View { Divider() - List(SettingsSection.allCases, selection: $selection) { section in - Label(section.title, systemImage: section.icon) - .tag(section) + List(selection: $selection) { + ForEach(SettingsCategory.allCases) { category in + let sections = SettingsSection.allCases.filter { $0.category == category } + Section { + ForEach(sections) { section in + Label(section.title, systemImage: section.icon) + .tag(section) + } + } header: { + if let title = category.title { + Text(title) + } + } + } } .listStyle(.sidebar) diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index d62b4f1..3817cc1 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -62,13 +62,29 @@ struct SettingsView: View { private var sectionDetail: some View { switch section { case .appearance: appearanceDetail - case .account: accountDetail + case .profile: profileDetail case .offlineSync: offlineSyncDetail case .advanced: advancedDetail case .about: aboutDetail + default: comingSoonDetail } } + /// Everything in Account/Workspace/Integrations & Installation that + /// isn't `.profile` — real content lands section by section; this is + /// just the nav skeleton until then. + private var comingSoonDetail: some View { + VStack(spacing: 16) { + sectionHeader + ContentUnavailableView { + Label("Coming Soon", systemImage: section.icon) + } description: { + Text("\(section.title) settings aren't built yet.") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + private var sectionHeader: some View { Text(section.title) .font(.title.bold()) @@ -90,9 +106,9 @@ struct SettingsView: View { } } - // MARK: - Account + // MARK: - Profile - private var accountDetail: some View { + private var profileDetail: some View { VStack(alignment: .leading, spacing: 16) { sectionHeader VStack(alignment: .leading, spacing: 10) { diff --git a/Outpost/Root/AppNavigation.swift b/Outpost/Root/AppNavigation.swift index 6804aef..32c7361 100644 --- a/Outpost/Root/AppNavigation.swift +++ b/Outpost/Root/AppNavigation.swift @@ -1,27 +1,130 @@ import Observation -enum SettingsSection: String, CaseIterable, Identifiable, Hashable { - case appearance, account, offlineSync, advanced, about +/// Top-level groupings shown as section headers in `SettingsSidebarList`. +/// `general` holds everything that's ours, not Outline's own settings +/// categories — no header, sits above the named groups. +enum SettingsCategory: String, CaseIterable, Identifiable { + case general + case account + case workspace + case integrationsInstallation var id: String { rawValue } + var title: String? { + switch self { + case .general: return nil + case .account: return "Account" + case .workspace: return "Workspace" + case .integrationsInstallation: return "Integrations & Installation" + } + } +} + +/// One entry in the Settings sidebar. Mirrors Outline's own settings +/// categories (Account/Workspace/Integrations & Installation) so this app's +/// settings read as a native counterpart to the web app's, plus a `general` +/// group for things that are ours and don't map onto Outline's structure +/// (offline/sync, advanced, about, appearance). +/// +/// Most of the Account/Workspace/Integrations cases are navigation-only for +/// now — `SettingsView` renders a "Coming Soon" placeholder for anything not +/// explicitly built yet. Content lands section by section. +enum SettingsSection: String, CaseIterable, Identifiable, Hashable { + // General (ours) + case appearance, offlineSync, advanced, about + + // Account + case profile, preferences, notifications, passkeys, apiAccess + + // Workspace + case details, authentication, security, ai, members, groups, templates, emojis, applications, shared, links, webhooks, importData, exportData + + // Integrations & Installation + case installation + + var id: String { rawValue } + + var category: SettingsCategory { + switch self { + case .appearance, .offlineSync, .advanced, .about: + return .general + case .profile, .preferences, .notifications, .passkeys, .apiAccess: + return .account + case .details, .authentication, .security, .ai, .members, .groups, .templates, .emojis, .applications, .shared, .links, .webhooks, .importData, .exportData: + return .workspace + case .installation: + return .integrationsInstallation + } + } + var title: String { switch self { case .appearance: return "Appearance" - case .account: return "Account" case .offlineSync: return "Offline & Sync" case .advanced: return "Advanced" case .about: return "About" + case .profile: return "Profile" + case .preferences: return "Preferences" + case .notifications: return "Notifications" + case .passkeys: return "Passkeys" + case .apiAccess: return "API & Access" + case .details: return "Details" + case .authentication: return "Authentication" + case .security: return "Security" + case .ai: return "AI" + case .members: return "Members" + case .groups: return "Groups" + case .templates: return "Templates" + case .emojis: return "Emojis" + case .applications: return "Applications" + case .shared: return "Shared" + case .links: return "Links" + case .webhooks: return "Webhooks" + case .importData: return "Import" + case .exportData: return "Export" + case .installation: return "Installation" } } var icon: String { switch self { case .appearance: return "paintbrush" - case .account: return "person.crop.circle" case .offlineSync: return "arrow.triangle.2.circlepath" case .advanced: return "wrench.and.screwdriver" case .about: return "info.circle" + case .profile: return "person.crop.circle" + case .preferences: return "gearshape" + case .notifications: return "bell" + case .passkeys: return "key" + case .apiAccess: return "chevron.left.forwardslash.chevron.right" + case .details: return "building.2" + case .authentication: return "lock" + case .security: return "shield" + case .ai: return "sparkles" + case .members: return "person.2" + case .groups: return "person.3" + case .templates: return "doc.on.doc" + case .emojis: return "face.smiling" + case .applications: return "app.badge" + case .shared: return "square.and.arrow.up.on.square" + case .links: return "link" + case .webhooks: return "bolt.horizontal" + case .importData: return "square.and.arrow.down" + case .exportData: return "square.and.arrow.up" + case .installation: return "shippingbox" + } + } + + /// Everything actually built so far — everything else in Account/ + /// Workspace/Integrations & Installation renders a "Coming Soon" + /// placeholder until its content is specified and built. + var isImplemented: Bool { + switch self { + case .appearance, .offlineSync, .advanced, .about, .profile: + return true + default: + return false } } }