Fix theme selection bug and update onboarding with AppTheme picker
- Change Theme enum from Int to String raw values to fix theme selection bug - Replace OnboardingStyle icon/color pickers with unified AppTheme grid - Remove visible text labels from voting layouts while keeping accessibility labels (WCAG 2.1 AA compliant) - Update widget voting views to use icons only with proper accessibility support - Consolidate app icons to single unified icon set 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,27 +146,21 @@ struct CardVotingView: View {
|
||||
LazyVGrid(columns: columns, spacing: 12) {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(action: { onMoodSelected(mood) }) {
|
||||
VStack(spacing: 8) {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.strValue)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(moodTint.color(forMood: mood).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(moodTint.color(forMood: mood).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.accessibilityLabel(mood.strValue)
|
||||
@@ -195,22 +189,16 @@ struct RadialVotingView: View {
|
||||
let position = positionForAngle(angle, radius: radius, center: center)
|
||||
|
||||
Button(action: { onMoodSelected(mood) }) {
|
||||
VStack(spacing: 4) {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.strValue)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
.padding(8)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.1))
|
||||
)
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.padding(12)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.1))
|
||||
)
|
||||
}
|
||||
.buttonStyle(MoodButtonStyle())
|
||||
.position(position)
|
||||
@@ -312,59 +300,51 @@ struct AuraVotingView: View {
|
||||
let color = moodTint.color(forMood: mood)
|
||||
|
||||
return Button(action: { onMoodSelected(mood) }) {
|
||||
VStack(spacing: 10) {
|
||||
// Glowing orb
|
||||
ZStack {
|
||||
// Outer atmospheric glow
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.5),
|
||||
color.opacity(0.2),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 45
|
||||
)
|
||||
// Glowing orb
|
||||
ZStack {
|
||||
// Outer atmospheric glow
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.5),
|
||||
color.opacity(0.2),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 45
|
||||
)
|
||||
.frame(width: 90, height: 90)
|
||||
)
|
||||
.frame(width: 90, height: 90)
|
||||
|
||||
// Middle glow ring
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.8),
|
||||
color.opacity(0.4)
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 10,
|
||||
endRadius: 30
|
||||
)
|
||||
// Middle glow ring
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.8),
|
||||
color.opacity(0.4)
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 10,
|
||||
endRadius: 30
|
||||
)
|
||||
.frame(width: 60, height: 60)
|
||||
)
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
// Inner solid core
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 48, height: 48)
|
||||
.shadow(color: color.opacity(0.8), radius: 12, x: 0, y: 0)
|
||||
// Inner solid core
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 48, height: 48)
|
||||
.shadow(color: color.opacity(0.8), radius: 12, x: 0, y: 0)
|
||||
|
||||
// Icon
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 26, height: 26)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// Label with elegant typography
|
||||
Text(mood.strValue)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(color)
|
||||
.tracking(0.5)
|
||||
// Icon
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 26, height: 26)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.buttonStyle(AuraButtonStyle(color: color))
|
||||
@@ -687,66 +667,60 @@ struct NeonEqualizerBar: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(spacing: 8) {
|
||||
// The equalizer bar
|
||||
ZStack(alignment: .bottom) {
|
||||
// Glow background
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(barColor.opacity(pulsePhase ? 0.15 : 0.08))
|
||||
.frame(height: barHeight + 20)
|
||||
.blur(radius: 15)
|
||||
// The equalizer bar
|
||||
ZStack(alignment: .bottom) {
|
||||
// Glow background
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(barColor.opacity(pulsePhase ? 0.15 : 0.08))
|
||||
.frame(height: barHeight + 20)
|
||||
.blur(radius: 15)
|
||||
|
||||
// Main bar
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
barColor,
|
||||
barColor.opacity(0.7)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
// Main bar
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
barColor,
|
||||
barColor.opacity(0.7)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
.shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0)
|
||||
.shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5)
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
.shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0)
|
||||
.shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5)
|
||||
|
||||
// Top highlight
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color.white.opacity(0.5), Color.clear],
|
||||
startPoint: .top,
|
||||
endPoint: .center
|
||||
)
|
||||
// Top highlight
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color.white.opacity(0.5), Color.clear],
|
||||
startPoint: .top,
|
||||
endPoint: .center
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
)
|
||||
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
|
||||
|
||||
// Level indicators (horizontal lines)
|
||||
VStack(spacing: 8) {
|
||||
ForEach(0..<Int(barHeight / 15), id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.frame(height: 2)
|
||||
}
|
||||
// Level indicators (horizontal lines)
|
||||
VStack(spacing: 8) {
|
||||
ForEach(0..<Int(barHeight / 15), id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.frame(height: 2)
|
||||
}
|
||||
.frame(height: isPressed ? barHeight * 0.9 - 10 : barHeight - 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
.frame(maxHeight: 160, alignment: .bottom)
|
||||
|
||||
// Mood label
|
||||
Text(mood.shortLabel)
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(barColor)
|
||||
.shadow(color: barColor.opacity(0.8), radius: 4, x: 0, y: 0)
|
||||
.frame(height: isPressed ? barHeight * 0.9 - 10 : barHeight - 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
.frame(maxHeight: 180, alignment: .bottom)
|
||||
}
|
||||
.buttonStyle(NeonBarButtonStyle(isPressed: $isPressed))
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Select this mood"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,15 +737,3 @@ struct NeonBarButtonStyle: ButtonStyle {
|
||||
}
|
||||
}
|
||||
|
||||
private extension Mood {
|
||||
var shortLabel: String {
|
||||
switch self {
|
||||
case .great: return "GRT"
|
||||
case .good: return "GUD"
|
||||
case .average: return "AVG"
|
||||
case .bad: return "BAD"
|
||||
case .horrible: return "HRB"
|
||||
default: return "---"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ struct CustomizeContentView: View {
|
||||
}
|
||||
|
||||
// WIDGETS
|
||||
SettingsSection(title: "Widgets") {
|
||||
CustomWidgetSection()
|
||||
}
|
||||
// SettingsSection(title: "Widgets") {
|
||||
// CustomWidgetSection()
|
||||
// }
|
||||
|
||||
// NOTIFICATIONS
|
||||
SettingsSection(title: "Notifications") {
|
||||
@@ -208,9 +208,9 @@ struct CustomizeView: View {
|
||||
}
|
||||
|
||||
// WIDGETS
|
||||
SettingsSection(title: "Widgets") {
|
||||
CustomWidgetSection()
|
||||
}
|
||||
// SettingsSection(title: "Widgets") {
|
||||
// CustomWidgetSection()
|
||||
// }
|
||||
|
||||
// NOTIFICATIONS
|
||||
SettingsSection(title: "Notifications") {
|
||||
@@ -330,7 +330,7 @@ struct ThemePickerCompact: View {
|
||||
.foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -9,42 +9,19 @@ import SwiftUI
|
||||
|
||||
struct ThemePickerView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
|
||||
@State private var selectedTheme: Theme = UserDefaultsStore.theme()
|
||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
selectedTheme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(Theme.allCases, id:\.rawValue) { aTheme in
|
||||
Button(action: {
|
||||
theme = aTheme
|
||||
changeTextColor(forTheme: aTheme)
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": aTheme.rawValue])
|
||||
}, label: {
|
||||
VStack {
|
||||
aTheme.currentTheme.preview
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color(UIColor.systemGray), style: StrokeStyle(lineWidth: 2))
|
||||
)
|
||||
Text(aTheme.title)
|
||||
.foregroundColor(textColor)
|
||||
.font(.body)
|
||||
}
|
||||
})
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(theme == aTheme ? theme.currentTheme.bgColor : .clear)
|
||||
.padding(-5)
|
||||
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
HStack(spacing: 0) {
|
||||
themeButton(for: .system)
|
||||
themeButton(for: .iFeel)
|
||||
themeButton(for: .dark)
|
||||
themeButton(for: .light)
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
@@ -52,8 +29,51 @@ struct ThemePickerView: View {
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.onAppear {
|
||||
selectedTheme = UserDefaultsStore.theme()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func themeButton(for theme: Theme) -> some View {
|
||||
Button {
|
||||
selectTheme(theme)
|
||||
} label: {
|
||||
VStack {
|
||||
theme.currentTheme.preview
|
||||
.allowsHitTesting(false)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color(UIColor.systemGray), style: StrokeStyle(lineWidth: 2))
|
||||
)
|
||||
Text(theme.title)
|
||||
.foregroundColor(textColor)
|
||||
.font(.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(selectedTheme == theme ? selectedTheme.currentTheme.bgColor : .clear)
|
||||
.padding(-5)
|
||||
)
|
||||
}
|
||||
|
||||
private func selectTheme(_ theme: Theme) {
|
||||
// Save theme value
|
||||
GroupUserDefaults.groupDefaults.set(theme.rawValue, forKey: UserDefaultsStore.Keys.theme.rawValue)
|
||||
GroupUserDefaults.groupDefaults.synchronize()
|
||||
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
selectedTheme = theme
|
||||
}
|
||||
|
||||
changeTextColor(forTheme: theme)
|
||||
EventLogger.log(event: "change_theme_id", withData: ["id": theme.rawValue])
|
||||
}
|
||||
|
||||
private func changeTextColor(forTheme theme: Theme) {
|
||||
if [Theme.iFeel, Theme.system].contains(theme) {
|
||||
let currentSystemScheme = UITraitCollection.current.userInterfaceStyle
|
||||
|
||||
Reference in New Issue
Block a user