Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,28 @@ jobs:
-destination 'platform=iOS Simulator,OS=26.2,name=iPhone 17' \
-only-testing:KleponSnapshotTests \
CODE_SIGNING_ALLOWED=NO

macos-build:
name: macOS app build
runs-on: macos-26

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install XcodeGen
run: brew install xcodegen

- name: Generate project
run: xcodegen generate

- name: Resolve Swift packages
run: xcodebuild -project Klepon.xcodeproj -scheme 'Klepon Mac' -resolvePackageDependencies

- name: Build macOS app
run: |
xcodebuild build \
-project Klepon.xcodeproj \
-scheme 'Klepon Mac' \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO
258 changes: 258 additions & 0 deletions Klepon.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions Klepon.xcodeproj/xcshareddata/xcschemes/Klepon Mac.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "676CA86C78E7DB801DEEAF12"
BuildableName = "Klepon.app"
BlueprintName = "Klepon Mac"
ReferencedContainer = "container:Klepon.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "676CA86C78E7DB801DEEAF12"
BuildableName = "Klepon.app"
BlueprintName = "Klepon Mac"
ReferencedContainer = "container:Klepon.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "676CA86C78E7DB801DEEAF12"
BuildableName = "Klepon.app"
BlueprintName = "Klepon Mac"
ReferencedContainer = "container:Klepon.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "676CA86C78E7DB801DEEAF12"
BuildableName = "Klepon.app"
BlueprintName = "Klepon Mac"
ReferencedContainer = "container:Klepon.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
2 changes: 1 addition & 1 deletion Klepon/AI/GuideAnswerService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class GuideAnswerService {
let previousContext = previousConversationContext(from: previousCards)

return """
You are answering inside Klepon, a private iPhone guide to Indonesian food.
You are answering inside Klepon, a private local guide to Indonesian food.
Use only the guide notes below.
If the notes are not enough, say the guide does not have enough detail yet.
Keep the tone warm, respectful, and concise.
Expand Down
8 changes: 4 additions & 4 deletions Klepon/AI/OndeGuideEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ enum PrivateGuideAvailability: Equatable {
case .notInstalled:
return "Private guide not ready yet"
case .downloaded:
return "Private guide downloaded on this iPhone"
return "Private guide downloaded on this device"
case .preparing:
return "Preparing your private guide"
case .ready:
return "Private guide ready on this iPhone"
return "Private guide ready on this device"
case .answering:
return "Answering privately"
case .failed:
Expand All @@ -34,10 +34,10 @@ enum PrivateGuideAvailability: Equatable {
"You can browse everything first, then add a one-time private guide download whenever you want deeper follow-up answers."
case .downloaded:
return
"The private guide is already stored on this iPhone. Finish preparing it when you want faster follow-up answers in the app."
"The private guide is already stored on this device. Finish preparing it when you want faster follow-up answers in the app."
case .preparing:
return
"The first run can take a while because Klepon needs to download and load the private guide locally on your iPhone."
"The first run can take a while because Klepon needs to download and load the private guide locally on your device."
case .ready:
return
"Klepon can now answer follow-up questions privately without turning the app into a generic chat shell."
Expand Down
11 changes: 10 additions & 1 deletion Klepon/App/AppRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ struct AppRootView: View {
NavigationStack {
SettingsView()
}
#if os(macOS)
.frame(minWidth: 480, minHeight: 620)
#endif
}
.fullScreenCover(isPresented: $showingOnboarding) {
.kleponOnboardingPresentation(isPresented: $showingOnboarding) {
OnboardingView(
onBrowseFirst: {
appState.completeOnboarding()
Expand All @@ -55,6 +58,9 @@ struct AppRootView: View {
appState.completeOnboarding()
}
)
#if os(macOS)
.frame(minWidth: 640, minHeight: 760)
#endif
}
.task {
showingOnboarding = !appState.hasCompletedOnboarding
Expand All @@ -63,5 +69,8 @@ struct AppRootView: View {
.onChange(of: appState.hasCompletedOnboarding) { _, newValue in
showingOnboarding = !newValue
}
#if os(macOS)
.frame(minWidth: 980, minHeight: 720)
#endif
}
}
7 changes: 6 additions & 1 deletion Klepon/App/KleponApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ struct KleponApp: App {
.environmentObject(appState.recentSearchStore)
.environmentObject(appState.recentlyViewedStore)
.environmentObject(appState.guideEngine)
.preferredColorScheme(.light)
#if os(iOS)
.preferredColorScheme(.light)
#endif
}
#if os(macOS)
.defaultSize(width: 1100, height: 760)
#endif
}
}
43 changes: 43 additions & 0 deletions Klepon/App/PlatformViewSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import SwiftUI

extension ToolbarItemPlacement {
static var kleponPrimaryAction: ToolbarItemPlacement {
#if os(macOS)
return .primaryAction
#else
return .topBarTrailing
#endif
}
}

extension View {
@ViewBuilder
func kleponInlineNavigationTitle() -> some View {
#if os(iOS)
self.navigationBarTitleDisplayMode(.inline)
#else
self
#endif
}

@ViewBuilder
func kleponLargeNavigationTitle() -> some View {
#if os(iOS)
self.navigationBarTitleDisplayMode(.large)
#else
self
#endif
}

@ViewBuilder
func kleponOnboardingPresentation<Content: View>(
isPresented: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
) -> some View {
#if os(macOS)
self.sheet(isPresented: isPresented, content: content)
#else
self.fullScreenCover(isPresented: isPresented, content: content)
#endif
}
}
18 changes: 18 additions & 0 deletions Klepon/Assets.xcassets/AppIconMac.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{ "idiom" : "mac", "size" : "16x16", "scale" : "1x", "filename" : "Icon-16.png" },
{ "idiom" : "mac", "size" : "16x16", "scale" : "2x", "filename" : "[email protected]" },
{ "idiom" : "mac", "size" : "32x32", "scale" : "1x", "filename" : "Icon-32.png" },
{ "idiom" : "mac", "size" : "32x32", "scale" : "2x", "filename" : "[email protected]" },
{ "idiom" : "mac", "size" : "128x128", "scale" : "1x", "filename" : "Icon-128.png" },
{ "idiom" : "mac", "size" : "128x128", "scale" : "2x", "filename" : "[email protected]" },
{ "idiom" : "mac", "size" : "256x256", "scale" : "1x", "filename" : "Icon-256.png" },
{ "idiom" : "mac", "size" : "256x256", "scale" : "2x", "filename" : "[email protected]" },
{ "idiom" : "mac", "size" : "512x512", "scale" : "1x", "filename" : "Icon-512.png" },
{ "idiom" : "mac", "size" : "512x512", "scale" : "2x", "filename" : "[email protected]" }
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions Klepon/Features/Ask/AskSheetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ struct AskSheetView: View {
}
.background(KleponColor.background.ignoresSafeArea())
.navigationTitle("Ask privately")
.navigationBarTitleDisplayMode(.inline)
.kleponInlineNavigationTitle()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .kleponPrimaryAction) {
Button("Done") {
dismiss()
}
Expand Down Expand Up @@ -212,7 +212,7 @@ private struct AnswerCardView: View {
Spacer(minLength: 12)

if card.isGeneratedOnDevice {
KleponChip(title: "On this iPhone", icon: "lock")
KleponChip(title: "On this device", icon: "lock")
} else {
KleponChip(title: "Curated fallback", icon: "book")
}
Expand Down
4 changes: 2 additions & 2 deletions Klepon/Features/Detail/GuideDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ struct GuideDetailView: View {
}
.background(KleponColor.background.ignoresSafeArea())
.navigationTitle(entry.title)
.navigationBarTitleDisplayMode(.inline)
.kleponInlineNavigationTitle()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .kleponPrimaryAction) {
Button {
favoritesStore.toggle(entry.id)
} label: {
Expand Down
8 changes: 4 additions & 4 deletions Klepon/Features/Discover/DiscoverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct DiscoverView: View {
}

private var featuredTasteNotes: [String] {
["Private on your iPhone", "Curated first", "Warm follow-up answers"]
["Private on your device", "Curated first", "Warm follow-up answers"]
}

private var recentlyViewedEntries: [GuideEntry] {
Expand Down Expand Up @@ -104,9 +104,9 @@ struct DiscoverView: View {
}
.background(KleponColor.background.ignoresSafeArea())
.navigationTitle("Klepon")
.navigationBarTitleDisplayMode(.large)
.kleponLargeNavigationTitle()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .kleponPrimaryAction) {
Button {
showingSettings = true
} label: {
Expand All @@ -125,7 +125,7 @@ struct DiscoverView: View {
.foregroundStyle(KleponColor.textPrimary)

Text(
"Explore dishes, ingredients, and food traditions with a guide that feels calm, personal, and private on your iPhone."
"Explore dishes, ingredients, and food traditions with a guide that feels calm, personal, and private on your device."
)
.font(KleponTypography.body)
.foregroundStyle(KleponColor.textSecondary)
Expand Down
57 changes: 35 additions & 22 deletions Klepon/Features/Onboarding/OnboardingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct OnboardingView: View {

KleponCard {
onboardingPoint(
title: "Private on your iPhone",
title: "Private on your device",
detail:
"Private answers use a one-time on-device guide download. You can browse first and add it later.",
systemImage: "lock"
Expand Down Expand Up @@ -94,31 +94,44 @@ struct OnboardingView: View {
.padding(.bottom, 180)
}
.background(KleponColor.background.ignoresSafeArea())
.safeAreaInset(edge: .bottom) {
VStack(spacing: 12) {
KleponActionButton(
title: privateGuideButtonTitle,
systemImage: "lock",
isLoading: guideEngine.availability.isBusy,
isDisabled: guideEngine.availability == .ready
) {
Task {
await guideEngine.prepareIfNeeded(
forceReload: guideEngine.availability.isFailure)
if guideEngine.availability == .ready {
onComplete()
}
#if os(macOS)
.safeAreaInset(edge: .bottom) {
onboardingActions
.padding(.horizontal, 24)
.padding(.vertical, 16)
.background(KleponColor.background)
}
#else
.safeAreaInset(edge: .bottom) {
onboardingActions
.padding(.horizontal, 24)
.padding(.top, 12)
.padding(.bottom, 16)
.background(.ultraThinMaterial)
}
#endif
}

private var onboardingActions: some View {
VStack(spacing: 12) {
KleponActionButton(
title: privateGuideButtonTitle,
systemImage: "lock",
isLoading: guideEngine.availability.isBusy,
isDisabled: guideEngine.availability == .ready
) {
Task {
await guideEngine.prepareIfNeeded(
forceReload: guideEngine.availability.isFailure)
if guideEngine.availability == .ready {
onComplete()
}
}
}

KleponActionButton(title: "Browse first", tone: .secondary) {
onBrowseFirst()
}
KleponActionButton(title: "Browse first", tone: .secondary) {
onBrowseFirst()
}
.padding(.horizontal, 24)
.padding(.top, 12)
.padding(.bottom, 16)
.background(.ultraThinMaterial)
}
}

Expand Down
Loading