diff --git a/Gaze.xcodeproj/project.pbxproj b/Gaze.xcodeproj/project.pbxproj index 6c0cd03..9e02c12 100644 --- a/Gaze.xcodeproj/project.pbxproj +++ b/Gaze.xcodeproj/project.pbxproj @@ -419,6 +419,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -453,6 +454,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Gaze/ContentView.swift b/Gaze/ContentView.swift deleted file mode 100644 index b445173..0000000 --- a/Gaze/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// Gaze -// -// Created by Mike Freno on 1/7/26. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview("Content View") { - ContentView() -} diff --git a/Gaze/Extensions/ViewExtensions.swift b/Gaze/Extensions/ViewExtensions.swift new file mode 100644 index 0000000..f5d622a --- /dev/null +++ b/Gaze/Extensions/ViewExtensions.swift @@ -0,0 +1,61 @@ +// +// ViewExtensions.swift +// Gaze +// +// Created by Mike Freno on 1/11/26. +// + +import SwiftUI + +extension View { + /// Applies a glass effect on macOS 26.0+, with a fallback visual treatment for earlier versions + @ViewBuilder + func glassEffectIfAvailable( + _ style: GlassStyle, + in shape: S + ) -> some View { + if #available(macOS 26.0, *) { + self.glassEffect(style.toGlass(), in: shape) + } else { + self.background { + shape + .fill(.ultraThinMaterial) + .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2) + } + } + } +} + +// MARK: - GlassStyle Wrapper +/// Cross-version wrapper for glass effect styling +struct GlassStyle { + private let tintColor: Color? + private let isInteractive: Bool + + static let regular = GlassStyle(tintColor: nil, isInteractive: false) + + private init(tintColor: Color?, isInteractive: Bool) { + self.tintColor = tintColor + self.isInteractive = isInteractive + } + + func tint(_ color: Color) -> GlassStyle { + GlassStyle(tintColor: color, isInteractive: isInteractive) + } + + func interactive() -> GlassStyle { + GlassStyle(tintColor: tintColor, isInteractive: true) + } + + @available(macOS 26.0, *) + func toGlass() -> Glass { + var glass = Glass.regular + if let tintColor = tintColor { + glass = glass.tint(tintColor) + } + if isInteractive { + glass = glass.interactive() + } + return glass + } +} diff --git a/Gaze/Views/Components/InfoBox.swift b/Gaze/Views/Components/InfoBox.swift index a61ad64..78929f2 100644 --- a/Gaze/Views/Components/InfoBox.swift +++ b/Gaze/Views/Components/InfoBox.swift @@ -33,7 +33,7 @@ struct InfoBox: View { .foregroundColor(.white) } .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) } } diff --git a/Gaze/Views/Onboarding/OnboardingContainerView.swift b/Gaze/Views/Containers/OnboardingContainerView.swift similarity index 95% rename from Gaze/Views/Onboarding/OnboardingContainerView.swift rename to Gaze/Views/Containers/OnboardingContainerView.swift index 4a491cf..fa65ffd 100644 --- a/Gaze/Views/Onboarding/OnboardingContainerView.swift +++ b/Gaze/Views/Containers/OnboardingContainerView.swift @@ -74,7 +74,7 @@ struct OnboardingContainerView: View { Image(systemName: "figure.stand") } - SettingsOnboardingView( + GeneralSetupView( launchAtLogin: $launchAtLogin, subtleReminderSize: $subtleReminderSize, isAppStoreVersion: Binding( @@ -111,7 +111,8 @@ struct OnboardingContainerView: View { ) .foregroundColor(.primary) } - .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable( + GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10)) } Button(action: { @@ -133,8 +134,9 @@ struct OnboardingContainerView: View { ) .foregroundColor(.white) } - .glassEffect( - .regular.tint(currentPage == 5 ? .green : .accentColor).interactive(), + .glassEffectIfAvailable( + GlassStyle.regular.tint(currentPage == 5 ? .green : .accentColor) + .interactive(), in: .rect(cornerRadius: 10)) } .padding(.horizontal, 40) diff --git a/Gaze/Views/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift similarity index 99% rename from Gaze/Views/SettingsWindowView.swift rename to Gaze/Views/Containers/SettingsWindowView.swift index 8782725..a4186f5 100644 --- a/Gaze/Views/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -79,7 +79,7 @@ struct SettingsWindowView: View { Label("User Timers", systemImage: "plus.circle") } - SettingsOnboardingView( + GeneralSetupView( launchAtLogin: $launchAtLogin, subtleReminderSize: $subtleReminderSize, isAppStoreVersion: Binding( diff --git a/Gaze/Views/MenuBar/MenuBarContentView.swift b/Gaze/Views/MenuBar/MenuBarContentView.swift index 4b330ab..2868f0d 100644 --- a/Gaze/Views/MenuBar/MenuBarContentView.swift +++ b/Gaze/Views/MenuBar/MenuBarContentView.swift @@ -29,8 +29,8 @@ struct MenuBarHoverButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .glassEffect( - isHovered ? .regular.tint(.accentColor.opacity(0.5)).interactive() : .regular, + .glassEffectIfAvailable( + isHovered ? GlassStyle.regular.tint(.accentColor.opacity(0.5)).interactive() : GlassStyle.regular, in: .rect(cornerRadius: 6) ) .contentShape(Rectangle()) @@ -316,8 +316,8 @@ struct TimerStatusRow: View { .padding(6) } .buttonStyle(.plain) - .glassEffect( - isHoveredDevTrigger ? .regular.tint(.yellow.opacity(0.5)) : .regular, + .glassEffectIfAvailable( + isHoveredDevTrigger ? GlassStyle.regular.tint(.yellow.opacity(0.5)) : GlassStyle.regular, in: .circle ) .help("Trigger \(type.displayName) reminder now (dev)") @@ -334,8 +334,8 @@ struct TimerStatusRow: View { .padding(6) } .buttonStyle(.plain) - .glassEffect( - isHoveredSkip ? .regular.tint(.accentColor.opacity(0.5)) : .regular, + .glassEffectIfAvailable( + isHoveredSkip ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, in: .circle ) .help("Skip to next \(type.displayName) reminder") @@ -345,8 +345,8 @@ struct TimerStatusRow: View { } .padding(.horizontal, 8) .padding(.vertical, 6) - .glassEffect( - isHoveredBody ? .regular.tint(.accentColor.opacity(0.5)) : .regular, + .glassEffectIfAvailable( + isHoveredBody ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, in: .rect(cornerRadius: 6) ) .padding(.horizontal, 8) @@ -415,8 +415,8 @@ struct InactiveTimerRow: View { .padding(.vertical, 6) } .buttonStyle(.plain) - .glassEffect( - isHovered ? .regular.tint(.accentColor.opacity(0.5)) : .regular, + .glassEffectIfAvailable( + isHovered ? GlassStyle.regular.tint(.accentColor.opacity(0.5)) : GlassStyle.regular, in: .rect(cornerRadius: 6) ) .padding(.horizontal, 8) @@ -473,8 +473,8 @@ struct UserTimerStatusRow: View { .padding(.vertical, 6) } .buttonStyle(.plain) - .glassEffect( - isHovered ? .regular.tint(timer.color.opacity(0.3)) : .regular, + .glassEffectIfAvailable( + isHovered ? GlassStyle.regular.tint(timer.color.opacity(0.3)) : GlassStyle.regular, in: .rect(cornerRadius: 6) ) .padding(.horizontal, 8) diff --git a/Gaze/Views/Onboarding/BlinkSetupView.swift b/Gaze/Views/Setup/BlinkSetupView.swift similarity index 94% rename from Gaze/Views/Onboarding/BlinkSetupView.swift rename to Gaze/Views/Setup/BlinkSetupView.swift index 98759ed..0068e82 100644 --- a/Gaze/Views/Onboarding/BlinkSetupView.swift +++ b/Gaze/Views/Setup/BlinkSetupView.swift @@ -54,7 +54,7 @@ struct BlinkSetupView: View { .foregroundColor(.white) } .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) VStack(alignment: .leading, spacing: 20) { Toggle("Enable Blink Reminders", isOn: $enabled) @@ -81,7 +81,7 @@ struct BlinkSetupView: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) if enabled { Text( @@ -111,7 +111,7 @@ struct BlinkSetupView: View { .padding(.horizontal, 16) .padding(.vertical, 10) } - .glassEffect(.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) } Spacer() diff --git a/Gaze/Views/Onboarding/CompletionView.swift b/Gaze/Views/Setup/CompletionView.swift similarity index 96% rename from Gaze/Views/Onboarding/CompletionView.swift rename to Gaze/Views/Setup/CompletionView.swift index b7d4b5f..2b309a3 100644 --- a/Gaze/Views/Onboarding/CompletionView.swift +++ b/Gaze/Views/Setup/CompletionView.swift @@ -67,7 +67,7 @@ struct CompletionView: View { .padding(.horizontal) } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) Spacer() } diff --git a/Gaze/Views/Onboarding/SettingsOnboardingView.swift b/Gaze/Views/Setup/GeneralSetupView.swift similarity index 89% rename from Gaze/Views/Onboarding/SettingsOnboardingView.swift rename to Gaze/Views/Setup/GeneralSetupView.swift index 856f19c..df0de1c 100644 --- a/Gaze/Views/Onboarding/SettingsOnboardingView.swift +++ b/Gaze/Views/Setup/GeneralSetupView.swift @@ -1,5 +1,5 @@ // -// SettingsOnboardingView.swift +// GeneralSetupView.swift // Gaze // // Created by Mike Freno on 1/8/26. @@ -7,7 +7,7 @@ import SwiftUI -struct SettingsOnboardingView: View { +struct GeneralSetupView: View { @Binding var launchAtLogin: Bool @Binding var subtleReminderSize: ReminderSize @Binding var isAppStoreVersion: Bool @@ -50,7 +50,7 @@ struct SettingsOnboardingView: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) VStack(alignment: .leading, spacing: 12) { Text("Subtle Reminder Size") @@ -87,17 +87,17 @@ struct SettingsOnboardingView: View { .frame(maxWidth: .infinity, minHeight: 60) .padding(.vertical, 12) } - .glassEffect( + .glassEffectIfAvailable( subtleReminderSize == size - ? .regular.tint(.accentColor.opacity(0.3)) - : .regular, + ? GlassStyle.regular.tint(.accentColor.opacity(0.3)) + : GlassStyle.regular, in: .rect(cornerRadius: 10) ) } } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) // Links Section VStack(spacing: 12) { @@ -130,7 +130,8 @@ struct SettingsOnboardingView: View { .frame(maxWidth: .infinity) } .buttonStyle(.plain) - .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable( + GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10)) if !isAppStoreVersion { Button(action: { @@ -160,8 +161,9 @@ struct SettingsOnboardingView: View { .cornerRadius(10) } .buttonStyle(.plain) - .glassEffect( - .regular.tint(.orange).interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable( + GlassStyle.regular.tint(.orange).interactive(), + in: .rect(cornerRadius: 10)) } } .padding() @@ -195,20 +197,11 @@ struct SettingsOnboardingView: View { } } -#Preview("Settings Onboarding - Launch Disabled") { - SettingsOnboardingView( +#Preview("Settings Onboarding") { + GeneralSetupView( launchAtLogin: .constant(false), subtleReminderSize: .constant(.medium), isAppStoreVersion: .constant(false), isOnboarding: true ) } - -#Preview("Settings Onboarding - Launch Enabled") { - SettingsOnboardingView( - launchAtLogin: .constant(true), - subtleReminderSize: .constant(.medium), - isAppStoreVersion: .constant(false), - isOnboarding: true - ) -} diff --git a/Gaze/Views/Onboarding/LookAwaySetupView.swift b/Gaze/Views/Setup/LookAwaySetupView.swift similarity index 95% rename from Gaze/Views/Onboarding/LookAwaySetupView.swift rename to Gaze/Views/Setup/LookAwaySetupView.swift index fb54fde..a032f6b 100644 --- a/Gaze/Views/Onboarding/LookAwaySetupView.swift +++ b/Gaze/Views/Setup/LookAwaySetupView.swift @@ -58,7 +58,7 @@ struct LookAwaySetupView: View { .foregroundColor(.white) } .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) VStack(alignment: .leading, spacing: 20) { Toggle("Enable Look Away Reminders", isOn: $enabled) @@ -101,7 +101,7 @@ struct LookAwaySetupView: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) if enabled { Text( @@ -132,7 +132,7 @@ struct LookAwaySetupView: View { .padding(.horizontal, 16) .padding(.vertical, 10) } - .glassEffect(.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) } Spacer() diff --git a/Gaze/Views/Onboarding/PostureSetupView.swift b/Gaze/Views/Setup/PostureSetupView.swift similarity index 94% rename from Gaze/Views/Onboarding/PostureSetupView.swift rename to Gaze/Views/Setup/PostureSetupView.swift index 04fc2b7..cb702e8 100644 --- a/Gaze/Views/Onboarding/PostureSetupView.swift +++ b/Gaze/Views/Setup/PostureSetupView.swift @@ -56,7 +56,7 @@ struct PostureSetupView: View { .foregroundColor(.white) } .padding() - .glassEffect(.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor), in: .rect(cornerRadius: 8)) VStack(alignment: .leading, spacing: 20) { Toggle("Enable Posture Reminders", isOn: $enabled) @@ -83,7 +83,7 @@ struct PostureSetupView: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) if enabled { Text( @@ -113,7 +113,7 @@ struct PostureSetupView: View { .padding(.horizontal, 16) .padding(.vertical, 10) } - .glassEffect(.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 10)) } Spacer() diff --git a/Gaze/Views/Onboarding/UserTimersView.swift b/Gaze/Views/Setup/UserTimersView.swift similarity index 98% rename from Gaze/Views/Onboarding/UserTimersView.swift rename to Gaze/Views/Setup/UserTimersView.swift index 46bf20e..acf313d 100644 --- a/Gaze/Views/Onboarding/UserTimersView.swift +++ b/Gaze/Views/Setup/UserTimersView.swift @@ -40,7 +40,7 @@ struct UserTimersView: View { .foregroundColor(.white) } .padding() - .glassEffect(.regular.tint(.purple), in: .rect(cornerRadius: 8)) + .glassEffectIfAvailable(GlassStyle.regular.tint(.purple), in: .rect(cornerRadius: 8)) VStack(alignment: .leading, spacing: 12) { HStack { @@ -96,7 +96,7 @@ struct UserTimersView: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) } Spacer() } @@ -354,7 +354,7 @@ struct UserTimerEditSheet: View { } } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) HStack(spacing: 12) { Button("Cancel", action: onCancel) diff --git a/Gaze/Views/Onboarding/WelcomeView.swift b/Gaze/Views/Setup/WelcomeView.swift similarity index 96% rename from Gaze/Views/Onboarding/WelcomeView.swift rename to Gaze/Views/Setup/WelcomeView.swift index acb3ed7..f658a9e 100644 --- a/Gaze/Views/Onboarding/WelcomeView.swift +++ b/Gaze/Views/Setup/WelcomeView.swift @@ -38,7 +38,7 @@ struct WelcomeView: View { description: "Create your own timers for specific needs") } .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 12)) + .glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12)) Spacer() }