| 1 | import SwiftUI |
| 2 | |
| 3 | struct OnboardingView: View { |
| 4 | @EnvironmentObject private var guideEngine: OndeGuideEngine |
| 5 | |
| 6 | let onBrowseFirst: () -> Void |
| 7 | let onComplete: () -> Void |
| 8 | |
| 9 | private var privateGuideButtonTitle: String { |
| 10 | switch guideEngine.availability { |
| 11 | case .ready: |
| 12 | return "Private guide ready" |
| 13 | case .downloaded: |
| 14 | return "Finish preparing private guide" |
| 15 | case .failed: |
| 16 | return "Try private guide again" |
| 17 | case .unsupported: |
| 18 | return "Not available on this device" |
| 19 | default: |
| 20 | return "Add private guide" |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | var body: some View { |
| 25 | ScrollView { |
| 26 | VStack(alignment: .leading, spacing: 24) { |
| 27 | Spacer(minLength: 24) |
| 28 | |
| 29 | Text("Klepon") |
| 30 | .font(KleponTypography.heroTitle) |
| 31 | .foregroundStyle(KleponColor.textPrimary) |
| 32 | |
| 33 | Text("Taste of Indonesia") |
| 34 | .font(KleponTypography.screenTitle) |
| 35 | .foregroundStyle(KleponColor.accentWarm) |
| 36 | |
| 37 | Text("A warm pocket guide to Indonesian dishes, ingredients, and food traditions.") |
| 38 | .font(KleponTypography.body) |
| 39 | .foregroundStyle(KleponColor.textSecondary) |
| 40 | |
| 41 | KleponCard { |
| 42 | onboardingPoint( |
| 43 | title: "Curated before clever", |
| 44 | detail: |
| 45 | "Browse thoughtful dishes and food notes first. The AI layer stays in the background.", |
| 46 | systemImage: "fork.knife" |
| 47 | ) |
| 48 | } |
| 49 | |
| 50 | KleponCard { |
| 51 | onboardingPoint( |
| 52 | title: "Private on your device", |
| 53 | detail: |
| 54 | "Private answers use a one-time on-device guide download. You can browse first and add it later.", |
| 55 | systemImage: "lock" |
| 56 | ) |
| 57 | } |
| 58 | |
| 59 | KleponCard { |
| 60 | onboardingPoint( |
| 61 | title: "Made to feel calm", |
| 62 | detail: |
| 63 | "No accounts, no noisy feeds, and no generic chatbot shell. Just a clean guide you can keep close.", |
| 64 | systemImage: "sparkles" |
| 65 | ) |
| 66 | } |
| 67 | |
| 68 | KleponCard { |
| 69 | VStack(alignment: .leading, spacing: 10) { |
| 70 | Text("Private guide status") |
| 71 | .font(KleponTypography.cardTitle) |
| 72 | .foregroundStyle(KleponColor.textPrimary) |
| 73 | |
| 74 | Text(guideEngine.availability.title) |
| 75 | .font(KleponTypography.bodySecondary) |
| 76 | .foregroundStyle( |
| 77 | guideEngine.availability.isFailure |
| 78 | ? KleponColor.highlight : KleponColor.textSecondary) |
| 79 | |
| 80 | Text(guideEngine.availability.detail) |
| 81 | .font(KleponTypography.caption) |
| 82 | .foregroundStyle(KleponColor.textSecondary) |
| 83 | |
| 84 | HStack(spacing: 10) { |
| 85 | KleponChip( |
| 86 | title: guideEngine.estimatedDownloadDescription, |
| 87 | icon: "arrow.down.circle") |
| 88 | KleponChip( |
| 89 | title: guideEngine.storageUsedDescription, icon: "internaldrive") |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | .padding(.horizontal, 24) |
| 95 | .padding(.top, 24) |
| 96 | .padding(.bottom, 180) |
| 97 | } |
| 98 | .background(KleponColor.background.ignoresSafeArea()) |
| 99 | #if os(macOS) |
| 100 | .safeAreaInset(edge: .bottom) { |
| 101 | onboardingActions |
| 102 | .padding(.horizontal, 24) |
| 103 | .padding(.vertical, 16) |
| 104 | .background(KleponColor.background) |
| 105 | } |
| 106 | #else |
| 107 | .safeAreaInset(edge: .bottom) { |
| 108 | onboardingActions |
| 109 | .padding(.horizontal, 24) |
| 110 | .padding(.top, 12) |
| 111 | .padding(.bottom, 16) |
| 112 | .background(.ultraThinMaterial) |
| 113 | } |
| 114 | #endif |
| 115 | } |
| 116 | |
| 117 | private var onboardingActions: some View { |
| 118 | VStack(spacing: 12) { |
| 119 | KleponActionButton( |
| 120 | title: privateGuideButtonTitle, |
| 121 | systemImage: "lock", |
| 122 | isLoading: guideEngine.availability.isBusy, |
| 123 | isDisabled: guideEngine.availability == .ready |
| 124 | ) { |
| 125 | Task { |
| 126 | await guideEngine.prepareIfNeeded( |
| 127 | forceReload: guideEngine.availability.isFailure) |
| 128 | if guideEngine.availability == .ready { |
| 129 | onComplete() |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | KleponActionButton(title: "Browse first", tone: .secondary) { |
| 135 | onBrowseFirst() |
| 136 | } |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | private func onboardingPoint(title: String, detail: String, systemImage: String) -> some View { |
| 141 | HStack(alignment: .top, spacing: 14) { |
| 142 | Image(systemName: systemImage) |
| 143 | .font(.title3) |
| 144 | .foregroundStyle(KleponColor.accent) |
| 145 | .frame(width: 28) |
| 146 | |
| 147 | VStack(alignment: .leading, spacing: 6) { |
| 148 | Text(title) |
| 149 | .font(KleponTypography.cardTitle) |
| 150 | .foregroundStyle(KleponColor.textPrimary) |
| 151 | |
| 152 | Text(detail) |
| 153 | .font(KleponTypography.bodySecondary) |
| 154 | .foregroundStyle(KleponColor.textSecondary) |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | } |