feat(settings): grouped sidebar matching Outline's own settings categories
SettingsSection now carries a SettingsCategory (general/account/ workspace/integrationsInstallation) and SettingsSidebarList renders it as a grouped, headered list — general (ours: Appearance, Offline & Sync, Advanced, About) unlabeled at the top, then Account (Profile, Preferences, Notifications, Passkeys, API & Access), Workspace (Details, Authentication, Security, AI, Members, Groups, Templates, Emojis, Applications, Shared, Links, Webhooks, Import, Export), and Integrations & Installation (Installation). Nav skeleton only, per instruction — everything except the ones already built (renamed the old flat "Account" page to "Profile", its natural new home; content unchanged) shows a "Coming Soon" placeholder until each section's real content is specified and built one at a time.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user