| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- //
- // RootShellView.swift
- // indeed_ai_job_search
- //
- import SwiftUI
- struct RootShellView: View {
- let dependencies: AppDependencies
- @State private var selection: SidebarItem = .jobSearch
- @State private var jobSearchVM: JobSearchViewModel
- @State private var savedJobsVM: SavedJobsViewModel
- @State private var alertsVM: JobAlertsViewModel
- @State private var settingsVM: SettingsViewModel
- @State private var showFiltersSheet = false
- init(dependencies: AppDependencies) {
- self.dependencies = dependencies
- _jobSearchVM = State(initialValue: JobSearchViewModel(dependencies: dependencies))
- _savedJobsVM = State(initialValue: SavedJobsViewModel(dependencies: dependencies))
- _alertsVM = State(initialValue: JobAlertsViewModel(dependencies: dependencies))
- _settingsVM = State(initialValue: SettingsViewModel())
- }
- var body: some View {
- GeometryReader { proxy in
- let width = proxy.size.width
- let showInlineFilters = width >= AppSpacing.mediumBreakpoint && selection == .jobSearch
- HStack(spacing: 0) {
- AppSidebarView(
- selection: $selection,
- unreadAlertCount: alertsVM.unreadCount
- )
- .frame(width: AppSpacing.sidebarWidth)
- .overlay(alignment: .trailing) {
- Rectangle()
- .fill(AppColors.border)
- .frame(width: 1)
- }
- VStack(spacing: 0) {
- topUtilityBar
- detail(showInlineFilters: showInlineFilters)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- .background(AppColors.backgroundPrimary)
- }
- .preferredColorScheme(settingsVM.preferredColorScheme)
- .sheet(isPresented: $showFiltersSheet) {
- NavigationStack {
- FiltersPanelView(
- criteria: $jobSearchVM.criteria,
- onReset: { jobSearchVM.resetFilters() },
- onChange: { jobSearchVM.applyFiltersAndRefresh() }
- )
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Done") { showFiltersSheet = false }
- }
- }
- }
- .frame(minWidth: 360, minHeight: 560)
- }
- .sheet(isPresented: $alertsVM.showNotificationCenter) {
- NotificationCenterSheet(events: alertsVM.events)
- }
- .onChange(of: selection) { _, newValue in
- if newValue == .savedJobs {
- savedJobsVM.refreshFromStore()
- }
- }
- .background(keyboardShortcuts)
- }
- /// Notifications sit top-right above mid content and filters.
- private var topUtilityBar: some View {
- HStack(spacing: AppSpacing.sm) {
- Spacer(minLength: 0)
- Button {
- alertsVM.openNotificationCenter()
- } label: {
- Image(systemName: alertsVM.unreadCount > 0 ? "bell.badge" : "bell")
- .font(.system(size: 15, weight: .medium))
- .foregroundStyle(AppColors.textSecondary)
- .frame(width: 32, height: 32)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .help("Notification center")
- .accessibilityLabel("Notifications")
- }
- .padding(.horizontal, AppSpacing.xl)
- .frame(height: AppSpacing.windowChromeTop)
- .frame(maxWidth: .infinity)
- .background(AppColors.backgroundPrimary)
- }
- @ViewBuilder
- private func detail(showInlineFilters: Bool) -> some View {
- switch selection {
- case .jobSearch:
- AIJobSearchView(
- viewModel: jobSearchVM,
- showsInlineFilters: showInlineFilters,
- onRequestFiltersSheet: { showFiltersSheet = true }
- )
- case .savedJobs:
- SavedJobsView(viewModel: savedJobsVM) {
- selection = .jobSearch
- }
- case .jobAlerts:
- JobAlertsView(viewModel: alertsVM)
- case .settings:
- SettingsView(viewModel: settingsVM)
- }
- }
- private var keyboardShortcuts: some View {
- Button("") {
- selection = .jobSearch
- jobSearchVM.focusSearchField.toggle()
- }
- .keyboardShortcut("f", modifiers: .command)
- .opacity(0)
- .frame(width: 0, height: 0)
- .accessibilityHidden(true)
- }
- }
- #Preview("Root Shell") {
- RootShellView(dependencies: .preview)
- .frame(width: 1280, height: 820)
- }
|