// // EntryListView.swift // Reflect (iOS) // // Created by Trey Tartt on 3/6/22. // import SwiftUI import CoreMotion struct EntryListView: View { @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @Environment(\.colorScheme) private var colorScheme private var textColor: Color { theme.currentTheme.labelColor } public let entry: MoodEntryModel private var moodColor: Color { moodTint.color(forMood: entry.mood) } /// Text color that contrasts with the mood's background color private var moodContrastingTextColor: Color { moodTint.contrastingTextColor(forMood: entry.mood) } private var isMissing: Bool { entry.moodValue == Mood.missing.rawValue } private var hasNotes: Bool { if let notes = entry.notes, !notes.isEmpty { return true } return false } private var hasReflection: Bool { if let json = entry.reflectionJSON, let reflection = GuidedReflection.decode(from: json), reflection.answeredCount > 0 { return true } return false } // MARK: - Cached Date Strings (avoids repeated ICU/Calendar operations) private var dateCache: DateFormattingCache { DateFormattingCache.shared } private var cachedDay: String { dateCache.string(for: entry.forDate, format: .day) } private var cachedWeekdayWide: String { dateCache.string(for: entry.forDate, format: .weekdayWide) } private var cachedWeekdayAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayAbbreviated) } private var cachedWeekdayWideDay: String { dateCache.string(for: entry.forDate, format: .weekdayWideDay) } private var cachedMonthWide: String { dateCache.string(for: entry.forDate, format: .monthWide) } private var cachedMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .monthAbbreviated) } private var cachedMonthAbbrevDay: String { dateCache.string(for: entry.forDate, format: .monthAbbreviatedDay) } private var cachedMonthAbbrevYear: String { dateCache.string(for: entry.forDate, format: .monthAbbreviatedYear) } private var cachedMonthWideYear: String { dateCache.string(for: entry.forDate, format: .monthWideYear) } private var cachedWeekdayAbbrevMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayAbbrevMonthAbbrev) } private var cachedWeekdayWideMonthAbbrev: String { dateCache.string(for: entry.forDate, format: .weekdayWideMonthAbbrev) } private var cachedYearMonthDayDigits: String { dateCache.string(for: entry.forDate, format: .yearMonthDayDigits) } var body: some View { Group { switch dayViewStyle { case .classic: classicStyle case .minimal: minimalStyle case .compact: compactStyle case .bubble: bubbleStyle case .grid: gridStyle case .aura: auraStyle case .chronicle: chronicleStyle case .neon: neonStyle case .ink: inkStyle case .prism: prismStyle case .tape: tapeStyle case .morph: morphStyle case .stack: stackStyle case .wave: waveStyle case .pattern: patternStyle case .leather: leatherStyle case .glass: glassStyle case .motion: motionStyle case .micro: microStyle case .orbit: orbitStyle } } .overlay(alignment: .bottomTrailing) { if !isMissing && (hasNotes || hasReflection) { HStack(spacing: 4) { if hasNotes { Image(systemName: "note.text") .font(.caption2) .accessibilityHidden(true) } if hasReflection { Image(systemName: "sparkles") .font(.caption2) .accessibilityHidden(true) } } .foregroundStyle(.secondary) .padding(.trailing, 12) .padding(.bottom, 6) } } .accessibilityElement(children: .combine) .accessibilityIdentifier(AccessibilityID.DayView.entryRow(dateString: cachedYearMonthDayDigits)) .accessibilityLabel(accessibilityDescription) .accessibilityHint(isMissing ? String(localized: "Tap to log mood for this day") : String(localized: "Tap to view or edit")) .accessibilityAddTraits(.isButton) } private var accessibilityDescription: String { let dateString = DateFormattingCache.shared.string(for: entry.forDate, format: .dateFull) if isMissing { return String(localized: "\(dateString), no mood logged") } else { var description = "\(dateString), \(entry.mood.strValue)" if hasNotes { description += String(localized: ", has notes") } if hasReflection { description += String(localized: ", has reflection") } return description } } // MARK: - Classic Style (Original) private var classicStyle: some View { HStack(spacing: 16) { // Large mood icon with gradient background ZStack { Circle() .fill( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.3), Color.gray.opacity(0.1)] : [moodColor.opacity(0.8), moodColor.opacity(0.4)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 56, height: 56) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 32, height: 32) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } .shadow(color: isMissing ? .clear : moodColor.opacity(0.4), radius: 8, x: 0, y: 4) VStack(alignment: .leading, spacing: 6) { HStack(spacing: 6) { Text(Random.weekdayName(fromDate: entry.forDate)) .font(.body.weight(.semibold)) .foregroundColor(textColor) Text("•") .foregroundColor(textColor.opacity(0.4)) Text(Random.dayFormat(fromDate: entry.forDate)) .font(.body.weight(.medium)) .foregroundColor(textColor.opacity(0.8)) } if isMissing { Text(String(localized: "mood_value_missing_tap_to_add")) .font(.subheadline.weight(.medium)) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.subheadline.weight(.semibold)) .foregroundColor(moodColor) .padding(.horizontal, 10) .padding(.vertical, 4) .background( Capsule() .fill(moodColor.opacity(0.15)) ) } } Spacer() Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(textColor.opacity(0.3)) } .padding(.horizontal, 18) .padding(.vertical, 16) .background( RoundedRectangle(cornerRadius: 18) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) .shadow( color: isMissing ? .clear : moodColor.opacity(colorScheme == .dark ? 0.2 : 0.12), radius: 12, x: 0, y: 4 ) ) .overlay( RoundedRectangle(cornerRadius: 18) .stroke( isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.2), lineWidth: 1 ) ) } // MARK: - Minimal Style private var minimalStyle: some View { HStack(spacing: 14) { // Simple flat circle with icon Circle() .fill(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.15)) .frame(width: 44, height: 44) .overlay( imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 22, height: 22) .foregroundColor(isMissing ? .gray : moodColor) .accessibilityLabel(entry.mood.strValue) ) VStack(alignment: .leading, spacing: 3) { Text(cachedWeekdayWideDay) .font(.subheadline.weight(.medium)) .foregroundColor(textColor) if isMissing { Text(String(localized: "mood_value_missing_tap_to_add")) .font(.caption) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.caption.weight(.medium)) .foregroundColor(moodColor) } } Spacer() } .padding(.horizontal, 16) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 12) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) } // MARK: - Compact Style (Timeline) private var compactStyle: some View { HStack(spacing: 12) { // Timeline indicator VStack(spacing: 0) { Circle() .fill(isMissing ? Color.gray.opacity(0.4) : moodColor) .frame(width: 12, height: 12) } // Date column VStack(alignment: .leading, spacing: 0) { Text(cachedDay) .font(.title3.weight(.bold)) .foregroundColor(textColor) Text(cachedWeekdayAbbrev) .font(.caption2.weight(.medium)) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) } .frame(width: 36) // Mood indicator bar if isMissing { RoundedRectangle(cornerRadius: 6) .fill(Color.gray.opacity(0.15)) .frame(height: 32) .overlay( Text(String(localized: "mood_value_missing_tap_to_add")) .font(.caption.weight(.medium)) .foregroundColor(.gray) ) } else { RoundedRectangle(cornerRadius: 6) .fill(moodColor.opacity(0.2)) .frame(height: 32) .overlay( HStack(spacing: 6) { imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .foregroundColor(moodColor) .accessibilityLabel(entry.mood.strValue) Text(entry.moodString) .font(.subheadline.weight(.semibold)) .foregroundColor(moodColor) } ) } Spacer(minLength: 0) } .padding(.horizontal, 12) .padding(.vertical, 10) } // MARK: - Bubble Style private var bubbleStyle: some View { HStack(spacing: 0) { // Full-width colored background HStack(spacing: 14) { // Icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) VStack(alignment: .leading, spacing: 2) { Text(cachedWeekdayWideDay) .font(.subheadline.weight(.semibold)) .foregroundColor(isMissing ? textColor : moodContrastingTextColor) if isMissing { Text(String(localized: "mood_value_missing_tap_to_add")) .font(.caption) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.caption.weight(.medium)) .foregroundColor(moodContrastingTextColor.opacity(0.85)) } } Spacer() Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundColor(isMissing ? textColor.opacity(0.3) : moodContrastingTextColor.opacity(0.6)) } .padding(.horizontal, 18) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 16) .fill( isMissing ? (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)) : moodColor ) ) .shadow( color: isMissing ? .clear : moodColor.opacity(0.35), radius: 8, x: 0, y: 4 ) } } // MARK: - Grid Style (3 per row) private var gridStyle: some View { VStack(spacing: 6) { // Mood icon circle ZStack { Circle() .fill( isMissing ? Color.gray.opacity(0.2) : moodColor ) .aspectRatio(1, contentMode: .fit) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .padding(16) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } .shadow( color: isMissing ? .clear : moodColor.opacity(0.3), radius: 6, x: 0, y: 3 ) // Day number Text(cachedDay) .font(.subheadline.weight(.bold)) .foregroundColor(textColor) // Weekday abbreviation Text(cachedWeekdayAbbrev) .font(.caption2.weight(.medium)) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 14) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) } // MARK: - Aura Style (Atmospheric glowing entries) private var auraStyle: some View { HStack(spacing: 0) { // Giant day number - the visual hero Text(cachedDay) .font(.largeTitle.weight(.black)) .foregroundStyle( isMissing ? LinearGradient(colors: [Color.gray.opacity(0.3), Color.gray.opacity(0.15)], startPoint: .top, endPoint: .bottom) : LinearGradient(colors: [moodColor, moodColor.opacity(0.6)], startPoint: .top, endPoint: .bottom) ) .frame(width: 90) .shadow(color: isMissing ? .clear : moodColor.opacity(0.5), radius: 20, x: 0, y: 0) // Content area with glowing aura VStack(alignment: .leading, spacing: 8) { // Weekday with elegant typography Text(cachedWeekdayWide) .font(.caption.weight(.semibold)) .textCase(.uppercase) .tracking(2) .foregroundColor(textColor.opacity(0.5)) // Mood display with icon HStack(spacing: 10) { // Glowing mood orb ZStack { // Outer glow Circle() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.2), Color.clear] : [moodColor.opacity(0.4), Color.clear], center: .center, startRadius: 0, endRadius: 30 ) ) .frame(width: 60, height: 60) // Inner solid circle Circle() .fill( isMissing ? Color.gray.opacity(0.3) : moodColor ) .frame(width: 38, height: 38) // Icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 2) { if isMissing { Text(String(localized: "mood_value_missing_tap_to_add")) .font(.subheadline.weight(.medium)) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.title3.weight(.bold)) .foregroundColor(textColor) // Month context Text(cachedMonthWide) .font(.caption.weight(.medium)) .foregroundColor(textColor.opacity(0.4)) } } Spacer() // Subtle indicator if !isMissing { Circle() .fill(moodColor) .frame(width: 8, height: 8) .shadow(color: moodColor, radius: 4, x: 0, y: 0) } } } .padding(.leading, 4) .padding(.trailing, 16) } .padding(.vertical, 20) .padding(.horizontal, 16) .background( ZStack { // Base layer RoundedRectangle(cornerRadius: 24) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) // Mood glow overlay (subtle gradient at edges) if !isMissing { RoundedRectangle(cornerRadius: 24) .fill( LinearGradient( colors: [ moodColor.opacity(colorScheme == .dark ? 0.15 : 0.08), Color.clear, Color.clear ], startPoint: .leading, endPoint: .trailing ) ) } } ) .overlay( RoundedRectangle(cornerRadius: 24) .stroke( isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(colorScheme == .dark ? 0.3 : 0.2), lineWidth: 1 ) ) .shadow( color: isMissing ? Color.black.opacity(0.05) : moodColor.opacity(colorScheme == .dark ? 0.25 : 0.15), radius: 16, x: 0, y: 8 ) } // MARK: - Chronicle Style (Editorial/Magazine) private var chronicleStyle: some View { VStack(alignment: .leading, spacing: 0) { // Top rule line Rectangle() .fill(isMissing ? Color.gray.opacity(0.3) : moodColor) .frame(height: 3) HStack(alignment: .top, spacing: 16) { // Left column: Giant day number in serif VStack(alignment: .trailing, spacing: 0) { Text(cachedDay) .font(.largeTitle.weight(.regular)) .foregroundColor(textColor) .frame(width: 80) Text(cachedWeekdayAbbrevMonthAbbrev) .font(.caption2.weight(.regular)) .italic() .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) } // Vertical divider Rectangle() .fill(textColor.opacity(0.15)) .frame(width: 1) // Right column: Mood content VStack(alignment: .leading, spacing: 12) { if isMissing { Text("Entry Missing") .font(.title2.weight(.regular)) .italic() .foregroundColor(.gray) Text("Tap to record your mood for this day") .font(.caption.weight(.regular)) .foregroundColor(.gray.opacity(0.7)) } else { // Pull-quote style mood name HStack(spacing: 8) { Rectangle() .fill(moodColor) .frame(width: 4) Text("\"\(entry.moodString)\"") .font(.title.weight(.regular)) .italic() .foregroundColor(textColor) } // Icon and descriptor HStack(spacing: 10) { imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) .foregroundColor(moodColor) .accessibilityLabel(entry.mood.strValue) Text("Recorded mood entry") .font(.caption.weight(.regular)) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) .tracking(1.5) } } } .padding(.vertical, 8) Spacer() } .padding(.horizontal, 16) .padding(.vertical, 12) // Bottom rule line (thinner) Rectangle() .fill(textColor.opacity(0.1)) .frame(height: 1) } .background(colorScheme == .dark ? Color(.systemGray6) : .white) } // MARK: - Neon Style (Synthwave Arcade) private var neonStyle: some View { let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82) let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8) let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04) // Map mood to synthwave color spectrum let synthwaveColor: Color = { switch entry.mood { case .great: return neonCyan case .good: return Color(red: 0.0, green: 0.9, blue: 0.6) // Cyan-green case .average: return Color(red: 0.9, green: 0.9, blue: 0.2) // Neon yellow case .bad: return Color(red: 1.0, green: 0.5, blue: 0.3) // Neon orange case .horrible: return neonMagenta default: return Color(red: 0.9, green: 0.9, blue: 0.2) // Fallback yellow } }() return ZStack { // Deep black base RoundedRectangle(cornerRadius: 6) .fill(deepBlack) // Grid background pattern Canvas { context, size in let gridSpacing: CGFloat = 12 let gridColor = neonCyan.opacity(0.08) // Horizontal grid lines for y in stride(from: 0, through: size.height, by: gridSpacing) { var path = Path() path.move(to: CGPoint(x: 0, y: y)) path.addLine(to: CGPoint(x: size.width, y: y)) context.stroke(path, with: .color(gridColor), lineWidth: 0.5) } // Vertical grid lines for x in stride(from: 0, through: size.width, by: gridSpacing) { var path = Path() path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x, y: size.height)) context.stroke(path, with: .color(gridColor), lineWidth: 0.5) } } .clipShape(RoundedRectangle(cornerRadius: 6)) // Scanline overlay for CRT effect - simplified with gradient stripes Canvas { context, size in let lineHeight: CGFloat = 3 var y: CGFloat = 0 while y < size.height { let rect = CGRect(x: 0, y: y, width: size.width, height: 1) context.fill(Path(rect), with: .color(.white.opacity(0.015))) y += lineHeight } } .clipShape(RoundedRectangle(cornerRadius: 6)) // Content HStack(spacing: 16) { // Neon equalizer-style mood indicator ZStack { // Outer glow ring RoundedRectangle(cornerRadius: 10) .stroke( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.3)] : [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 2 ) .blur(radius: 4) // Inner border RoundedRectangle(cornerRadius: 10) .stroke( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.4)] : [neonCyan, neonMagenta], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1.5 ) // Mood icon with synthwave glow imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(isMissing ? .gray : synthwaveColor) .shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.9), radius: 8, x: 0, y: 0) .shadow(color: isMissing ? .clear : synthwaveColor.opacity(0.5), radius: 16, x: 0, y: 0) .accessibilityLabel(entry.mood.strValue) } .frame(width: 54, height: 54) VStack(alignment: .leading, spacing: 5) { // Date in cyan monospace Text(cachedYearMonthDayDigits) .font(.system(.caption, design: .monospaced).weight(.semibold)) .foregroundColor(neonCyan) .shadow(color: neonCyan.opacity(0.5), radius: 4, x: 0, y: 0) if isMissing { Text("NO_DATA") .font(.system(.headline, design: .monospaced).weight(.black)) .foregroundColor(.gray.opacity(0.6)) } else { // Mood text with synthwave gradient glow Text(entry.moodString.uppercased()) .font(.system(.headline, design: .monospaced).weight(.black)) .foregroundStyle( LinearGradient( colors: [neonCyan, synthwaveColor, neonMagenta], startPoint: .leading, endPoint: .trailing ) ) .shadow(color: synthwaveColor.opacity(0.8), radius: 6, x: 0, y: 0) .shadow(color: neonMagenta.opacity(0.3), radius: 12, x: 0, y: 0) } // Weekday in magenta Text(cachedWeekdayWide) .font(.system(.caption2, design: .monospaced).weight(.medium)) .foregroundColor(neonMagenta.opacity(0.7)) .textCase(.uppercase) } Spacer() // Equalizer bars indicator (mini visualization) if !isMissing { HStack(spacing: 2) { ForEach(0..<5, id: \.self) { index in let barHeight: CGFloat = { let moodIndex = Mood.allValues.firstIndex(of: entry.mood) ?? 2 let heights: [[CGFloat]] = [ [28, 22, 16, 10, 6], // Great [24, 28, 18, 12, 8], // Good [16, 20, 28, 20, 16], // Okay [8, 12, 18, 28, 24], // Bad [6, 10, 16, 22, 28] // Awful ] return heights[moodIndex][index] }() RoundedRectangle(cornerRadius: 1) .fill( LinearGradient( colors: [neonCyan, neonMagenta], startPoint: .top, endPoint: .bottom ) ) .frame(width: 3, height: barHeight) .shadow(color: neonCyan.opacity(0.5), radius: 2, x: 0, y: 0) } } .padding(.trailing, 4) } // Chevron with gradient glow Image(systemName: "chevron.right") .font(.subheadline.weight(.bold)) .foregroundStyle( isMissing ? AnyShapeStyle(Color.gray.opacity(0.4)) : AnyShapeStyle(LinearGradient( colors: [neonCyan, neonMagenta], startPoint: .top, endPoint: .bottom )) ) .shadow(color: isMissing ? .clear : neonCyan.opacity(0.5), radius: 4, x: 0, y: 0) } .padding(.horizontal, 18) .padding(.vertical, 14) // Cyan-to-magenta gradient border RoundedRectangle(cornerRadius: 6) .stroke( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)] : [neonCyan.opacity(0.7), neonMagenta.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) } // Outer glow effect .shadow(color: isMissing ? .clear : neonCyan.opacity(0.2), radius: 12, x: 0, y: 2) .shadow(color: isMissing ? .clear : neonMagenta.opacity(0.15), radius: 20, x: 0, y: 4) } // MARK: - Ink Style (Japanese Zen/Calligraphy) private var inkStyle: some View { HStack(spacing: 20) { // Ensō (Zen circle) representing mood ZStack { // Brush stroke circle effect Circle() .stroke( isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.7), style: StrokeStyle(lineWidth: 6, lineCap: .round) ) .frame(width: 56, height: 56) .rotationEffect(.degrees(-30)) // Gap in the circle (zen incomplete circle) Circle() .trim(from: 0, to: 0.85) .stroke( isMissing ? Color.gray.opacity(0.4) : moodColor, style: StrokeStyle(lineWidth: 4, lineCap: .round) ) .frame(width: 44, height: 44) .rotationEffect(.degrees(20)) // Small icon in center imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) .foregroundColor(isMissing ? .gray.opacity(0.5) : moodColor.opacity(0.8)) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 8) { // Day number with brush-like weight variation HStack(alignment: .firstTextBaseline, spacing: 4) { Text(cachedDay) .font(.title.weight(.thin)) .foregroundColor(textColor) VStack(alignment: .leading, spacing: 2) { Text(cachedMonthWide) .font(.caption2.weight(.light)) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) .tracking(2) Text(cachedWeekdayWide) .font(.caption2.weight(.light)) .foregroundColor(textColor.opacity(0.35)) } } if isMissing { Text("—") .font(.title3.weight(.ultraLight)) .foregroundColor(.gray.opacity(0.4)) } else { // Mood in delicate typography Text(entry.moodString) .font(.body.weight(.light)) .foregroundColor(moodColor) .tracking(1) } } Spacer() // Subtle ink dot indicator if !isMissing { Circle() .fill(moodColor.opacity(0.6)) .frame(width: 6, height: 6) } } .padding(.horizontal, 24) .padding(.vertical, 24) .background( ZStack { // Paper-like texture base RoundedRectangle(cornerRadius: 2) .fill(colorScheme == .dark ? Color(.systemGray6) : Color(white: 0.98)) // Subtle ink wash at edge if !isMissing { HStack { Rectangle() .fill( LinearGradient( colors: [moodColor.opacity(0.08), Color.clear], startPoint: .leading, endPoint: .trailing ) ) .frame(width: 80) Spacer() } } } ) .overlay( // Thin ink border RoundedRectangle(cornerRadius: 2) .stroke(textColor.opacity(0.08), lineWidth: 0.5) ) } // MARK: - Prism Style (Premium Glassmorphism) private var prismStyle: some View { ZStack { // Rainbow refraction edge effect RoundedRectangle(cornerRadius: 20) .fill( AngularGradient( colors: [.red, .orange, .yellow, .green, .blue, .purple, .red], center: .center ) ) .blur(radius: 8) .opacity(isMissing ? 0 : 0.4) // Frosted glass card RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill( LinearGradient( colors: [ Color.white.opacity(colorScheme == .dark ? 0.1 : 0.5), Color.white.opacity(colorScheme == .dark ? 0.05 : 0.2) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) ) // Content HStack(spacing: 16) { // Glass orb with mood ZStack { Circle() .fill(.ultraThinMaterial) .frame(width: 56, height: 56) Circle() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.3), Color.gray.opacity(0.1)] : [moodColor.opacity(0.6), moodColor.opacity(0.2)], center: .topLeading, startRadius: 0, endRadius: 40 ) ) .frame(width: 52, height: 52) // Light reflection Circle() .fill( LinearGradient( colors: [Color.white.opacity(0.6), Color.clear], startPoint: .topLeading, endPoint: .center ) ) .frame(width: 52, height: 52) .mask( Circle() .frame(width: 20, height: 20) .offset(x: -10, y: -10) ) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 26, height: 26) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 6) { Text(cachedWeekdayWide) .font(.subheadline.weight(.semibold)) .foregroundColor(textColor) HStack(spacing: 8) { Text(cachedMonthAbbrevDay) .font(.caption.weight(.medium)) .foregroundColor(textColor.opacity(0.6)) if !isMissing { Capsule() .fill(moodColor.opacity(0.2)) .frame(width: 4, height: 4) Text(entry.moodString) .font(.caption.weight(.semibold)) .foregroundColor(moodColor) } else { Text("Tap to add") .font(.caption) .foregroundColor(.gray) } } } Spacer() // Prismatic chevron Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundStyle( isMissing ? AnyShapeStyle(Color.gray.opacity(0.3)) : AnyShapeStyle( LinearGradient( colors: [moodColor, moodColor.opacity(0.5)], startPoint: .top, endPoint: .bottom ) ) ) } .padding(16) } .frame(height: 88) .shadow(color: isMissing ? .clear : moodColor.opacity(0.2), radius: 20, x: 0, y: 10) } // MARK: - Tape Style (Retro Cassette/Mixtape) private var tapeStyle: some View { HStack(spacing: 0) { // Track number column VStack { Text(cachedDay) .font(.title2.weight(.bold).monospaced()) .foregroundColor(isMissing ? .gray : moodColor) } .frame(width: 50) .padding(.vertical, 16) .background( Rectangle() .fill(isMissing ? Color.gray.opacity(0.1) : moodColor.opacity(0.15)) ) // Tape reel visualization HStack(spacing: 12) { // Left reel ZStack { Circle() .stroke(isMissing ? Color.gray.opacity(0.3) : moodColor.opacity(0.4), lineWidth: 3) .frame(width: 32, height: 32) Circle() .fill(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.2)) .frame(width: 18, height: 18) Circle() .fill(colorScheme == .dark ? Color(.systemGray5) : .white) .frame(width: 8, height: 8) } // Track info VStack(alignment: .leading, spacing: 4) { Text(cachedWeekdayWideMonthAbbrev) .font(.caption2.weight(.medium).monospaced()) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) if isMissing { Text("SIDE B - NO RECORDING") .font(.subheadline.weight(.bold)) .foregroundColor(.gray) } else { Text(entry.moodString.uppercased()) .font(.subheadline.weight(.black)) .foregroundColor(textColor) .tracking(1) } // Tape progress bar GeometryReader { geo in ZStack(alignment: .leading) { Capsule() .fill(textColor.opacity(0.1)) .frame(height: 4) Capsule() .fill(isMissing ? Color.gray : moodColor) .frame(width: geo.size.width * (isMissing ? 0.2 : 0.7), height: 4) } } .frame(height: 4) } Spacer() // Right reel ZStack { Circle() .stroke(isMissing ? Color.gray.opacity(0.3) : moodColor.opacity(0.4), lineWidth: 3) .frame(width: 32, height: 32) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .foregroundColor(isMissing ? .gray : moodColor) .accessibilityLabel(entry.mood.strValue) } } .padding(.horizontal, 16) .padding(.vertical, 14) } .background( RoundedRectangle(cornerRadius: 8) .fill(colorScheme == .dark ? Color(.systemGray6) : Color(white: 0.96)) ) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.3), lineWidth: 1) ) } // MARK: - Morph Style (Liquid Organic Blobs) private var morphStyle: some View { ZStack { // Organic blob background GeometryReader { geo in ZStack { // Main blob Ellipse() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.2), Color.gray.opacity(0.05)] : [moodColor.opacity(0.5), moodColor.opacity(0.1)], center: .center, startRadius: 0, endRadius: geo.size.width * 0.6 ) ) .frame(width: geo.size.width * 0.9, height: geo.size.height * 1.2) .offset(x: -geo.size.width * 0.1, y: 0) .blur(radius: 20) // Secondary blob if !isMissing { Ellipse() .fill(moodColor.opacity(0.3)) .frame(width: geo.size.width * 0.4, height: geo.size.height * 0.8) .offset(x: geo.size.width * 0.25, y: geo.size.height * 0.1) .blur(radius: 15) } } } // Content overlay HStack(spacing: 20) { // Morphing mood indicator ZStack { // Outer glow blob Circle() .fill(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.4)) .frame(width: 70, height: 70) .blur(radius: 10) // Inner solid Circle() .fill( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.4), Color.gray.opacity(0.2)] : [moodColor, moodColor.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 50, height: 50) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24, height: 24) .foregroundColor(moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 8) { // Date with organic flow HStack(spacing: 0) { Text(cachedDay) .font(.title.weight(.light)) .foregroundColor(textColor) VStack(alignment: .leading, spacing: 0) { Text(cachedMonthAbbrev) .font(.caption.weight(.medium)) .foregroundColor(textColor.opacity(0.6)) Text(cachedWeekdayAbbrev) .font(.caption2.weight(.regular)) .foregroundColor(textColor.opacity(0.4)) } .padding(.leading, 6) } if isMissing { Text("No mood recorded") .font(.subheadline.weight(.medium)) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.headline.weight(.semibold)) .foregroundColor(moodColor) } } Spacer() } .padding(20) } .frame(height: 110) .clipShape(RoundedRectangle(cornerRadius: 28)) .background( RoundedRectangle(cornerRadius: 28) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) } // MARK: - Stack Style (Layered Paper Notes) private var stackStyle: some View { ZStack { // Back paper layers (offset for depth) RoundedRectangle(cornerRadius: 12) .fill(colorScheme == .dark ? Color(.systemGray5) : Color(white: 0.92)) .offset(x: 4, y: 4) RoundedRectangle(cornerRadius: 12) .fill(colorScheme == .dark ? Color(.systemGray5).opacity(0.7) : Color(white: 0.95)) .offset(x: 2, y: 2) // Main note VStack(alignment: .leading, spacing: 0) { // Torn paper edge effect - simplified with Canvas Canvas { context, size in let segmentWidth = size.width / 10 let color = (isMissing ? Color.gray.opacity(0.3) : moodColor.opacity(0.6)) for i in 0..<10 { let height = CGFloat(3 + (i * 7) % 4) // Deterministic pseudo-random heights let rect = CGRect(x: CGFloat(i) * segmentWidth, y: 0, width: segmentWidth, height: height) context.fill(Path(rect), with: .color(color)) } } .frame(height: 6) HStack(spacing: 16) { // Handwritten-style date VStack(alignment: .center, spacing: 2) { Text(cachedDay) .font(.title.weight(.light)) .foregroundColor(textColor) Text(cachedWeekdayAbbrev) .font(.caption.weight(.medium)) .foregroundColor(textColor.opacity(0.5)) .textCase(.uppercase) } .frame(width: 60) // Vertical line divider (like notebook margin) Rectangle() .fill(Color.red.opacity(0.3)) .frame(width: 1) // Content area VStack(alignment: .leading, spacing: 8) { // Lined paper effect Text(cachedMonthWideYear) .font(.caption.weight(.regular)) .foregroundColor(textColor.opacity(0.4)) if isMissing { Text("nothing written...") .font(.headline.weight(.regular)) .italic() .foregroundColor(.gray) } else { HStack(spacing: 10) { imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24, height: 24) .foregroundColor(moodColor) .accessibilityLabel(entry.mood.strValue) Text(entry.moodString) .font(.title3.weight(.medium)) .foregroundColor(textColor) } } // Notebook lines VStack(spacing: 8) { ForEach(0..<2, id: \.self) { _ in Rectangle() .fill(Color.blue.opacity(0.1)) .frame(height: 1) } } } .padding(.vertical, 8) Spacer() } .padding(16) } .background( RoundedRectangle(cornerRadius: 12) .fill(colorScheme == .dark ? Color(.systemGray6) : Color(white: 0.98)) ) .shadow(color: Color.black.opacity(0.1), radius: 8, x: 2, y: 4) } } // MARK: - Wave Style (Horizontal Gradient River) private var waveStyle: some View { HStack(spacing: 0) { // Date column - minimal VStack(alignment: .trailing, spacing: 2) { Text(cachedDay) .font(.title.weight(.thin)) .foregroundColor(textColor) Text(cachedWeekdayAbbrev) .font(.caption2.weight(.medium)) .foregroundColor(textColor.opacity(0.4)) .textCase(.uppercase) } .frame(width: 50) .padding(.trailing, 12) // Wave gradient bar ZStack(alignment: .leading) { // Gradient wave RoundedRectangle(cornerRadius: 16) .fill( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)] : [ moodColor, moodColor.opacity(0.7), moodColor.opacity(0.4) ], startPoint: .leading, endPoint: .trailing ) ) // Wave texture overlay if !isMissing { GeometryReader { geo in Path { path in path.move(to: CGPoint(x: 0, y: geo.size.height * 0.5)) for x in stride(from: 0, to: geo.size.width, by: 10) { let y = geo.size.height * 0.5 + sin(x / 20) * 8 path.addLine(to: CGPoint(x: x, y: y)) } path.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height)) path.addLine(to: CGPoint(x: 0, y: geo.size.height)) path.closeSubpath() } .fill(Color.white.opacity(0.15)) } } // Content on wave HStack { // Icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) .padding(.leading, 16) if isMissing { Text("No entry") .font(.subheadline.weight(.medium)) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.body.weight(.semibold)) .foregroundColor(moodContrastingTextColor) } Spacer() // Month indicator Text(cachedMonthAbbrev) .font(.caption2.weight(.bold)) .foregroundColor(isMissing ? .gray : moodContrastingTextColor.opacity(0.7)) .padding(.trailing, 16) } } .frame(height: 64) .shadow( color: isMissing ? .clear : moodColor.opacity(0.4), radius: 12, x: 0, y: 6 ) } .padding(.vertical, 4) } // MARK: - Pattern Style (Mood icons as repeating background) private var patternStyle: some View { HStack(spacing: 16) { // Large mood icon ZStack { Circle() .fill(isMissing ? Color.gray.opacity(0.2) : moodColor) .frame(width: 54, height: 54) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 6) { Text(cachedWeekdayWideDay) .font(.body.weight(.semibold)) .foregroundColor(textColor) if isMissing { Text("No mood recorded") .font(.subheadline) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.subheadline.weight(.medium)) .foregroundColor(moodColor) } } .padding(.horizontal, 12) .padding(.vertical, 8) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 10)) Spacer() Text(cachedMonthAbbrev) .font(.caption.weight(.medium)) .foregroundColor(textColor.opacity(0.6)) .padding(.horizontal, 10) .padding(.vertical, 6) .background(.ultraThinMaterial, in: Capsule()) } .padding(16) .frame(height: 86) .background { // Simplified pattern background using Canvas for better performance Canvas { context, size in let iconSize: CGFloat = 20 let spacing: CGFloat = 28 let patternColor = (isMissing ? Color.gray : moodColor).opacity(0.15) // Draw simple circles as pattern instead of complex icons var row = 0 var y: CGFloat = 0 while y < size.height + spacing { var x: CGFloat = row.isMultiple(of: 2) ? spacing / 2 : 0 while x < size.width + spacing { let rect = CGRect(x: x - iconSize/2, y: y - iconSize/2, width: iconSize, height: iconSize) context.fill(Circle().path(in: rect), with: .color(patternColor)) x += spacing } y += spacing row += 1 } } .background( RoundedRectangle(cornerRadius: 16) .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5)) ) .clipShape(RoundedRectangle(cornerRadius: 16)) } .overlay( RoundedRectangle(cornerRadius: 16) .stroke(isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.3), lineWidth: 1) ) } // MARK: - Leather Style (Skeuomorphic) private var leatherStyle: some View { ZStack { // Leather texture base RoundedRectangle(cornerRadius: 12) .fill( LinearGradient( colors: [ Color(red: 0.45, green: 0.30, blue: 0.20), Color(red: 0.35, green: 0.22, blue: 0.15), Color(red: 0.40, green: 0.26, blue: 0.18) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Grain texture overlay RoundedRectangle(cornerRadius: 12) .fill( RadialGradient( colors: [Color.white.opacity(0.08), Color.clear, Color.black.opacity(0.1)], center: .topLeading, startRadius: 0, endRadius: 300 ) ) // Inner shadow (pressed effect) RoundedRectangle(cornerRadius: 10) .stroke(Color.black.opacity(0.3), lineWidth: 2) .blur(radius: 2) .offset(x: 1, y: 2) .mask(RoundedRectangle(cornerRadius: 12)) // Highlight on top edge VStack { RoundedRectangle(cornerRadius: 12) .fill(Color.white.opacity(0.15)) .frame(height: 2) Spacer() } .padding(.horizontal, 4) .padding(.top, 2) // Stitching border RoundedRectangle(cornerRadius: 10) .strokeBorder( style: StrokeStyle(lineWidth: 1.5, dash: [4, 3]) ) .foregroundColor(Color(red: 0.7, green: 0.55, blue: 0.35).opacity(0.6)) .padding(6) // Content HStack(spacing: 16) { // Embossed mood badge ZStack { // Outer ring shadow Circle() .fill(Color.black.opacity(0.3)) .frame(width: 50, height: 50) .offset(y: 2) .blur(radius: 2) // Metal ring Circle() .fill( LinearGradient( colors: [ Color(red: 0.85, green: 0.75, blue: 0.55), Color(red: 0.65, green: 0.55, blue: 0.35), Color(red: 0.85, green: 0.75, blue: 0.55) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 48, height: 48) Circle() .fill(isMissing ? Color.gray : moodColor) .frame(width: 40, height: 40) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 22, height: 22) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .shadow(color: Color.black.opacity(0.3), radius: 1, x: 0, y: 1) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 4) { Text(cachedWeekdayWide) .font(.subheadline.weight(.semibold)) .foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80)) .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1) Text(cachedMonthAbbrevDay) .font(.caption.weight(.medium)) .foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55)) if !isMissing { Text(entry.moodString) .font(.subheadline.weight(.bold)) .foregroundColor(Color(red: 0.95, green: 0.90, blue: 0.80)) .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1) } } Spacer() // Metal rivet detail ZStack { Circle() .fill(Color.black.opacity(0.4)) .frame(width: 10, height: 10) .offset(y: 1) Circle() .fill( RadialGradient( colors: [ Color(red: 0.9, green: 0.8, blue: 0.6), Color(red: 0.6, green: 0.5, blue: 0.3) ], center: .topLeading, startRadius: 0, endRadius: 6 ) ) .frame(width: 8, height: 8) } } .padding(.horizontal, 18) .padding(.vertical, 14) } .frame(height: 90) .shadow(color: Color.black.opacity(0.4), radius: 6, x: 0, y: 4) } // MARK: - Glass Style (iOS 26 Liquid Glass) private var glassStyle: some View { ZStack { // Variable blur background simulation RoundedRectangle(cornerRadius: 24) .fill(.ultraThinMaterial) // Glass refraction layer RoundedRectangle(cornerRadius: 24) .fill( LinearGradient( colors: [ Color.white.opacity(colorScheme == .dark ? 0.15 : 0.6), Color.white.opacity(colorScheme == .dark ? 0.05 : 0.2), Color.white.opacity(colorScheme == .dark ? 0.08 : 0.3) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) // Specular highlight (liquid glass shine) GeometryReader { geo in Ellipse() .fill( RadialGradient( colors: [Color.white.opacity(0.4), Color.clear], center: .center, startRadius: 0, endRadius: 80 ) ) .frame(width: 160, height: 50) .offset(x: -20, y: -10) .blur(radius: 10) } .clipped() // Content with depth HStack(spacing: 18) { // Floating glass orb for mood ZStack { // Orb shadow Circle() .fill(Color.black.opacity(0.15)) .frame(width: 54, height: 54) .offset(y: 4) .blur(radius: 8) // Glass orb Circle() .fill(.ultraThinMaterial) .frame(width: 52, height: 52) // Inner color Circle() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.4), Color.gray.opacity(0.2)] : [moodColor.opacity(0.7), moodColor.opacity(0.4)], center: .topLeading, startRadius: 0, endRadius: 35 ) ) .frame(width: 46, height: 46) // Glass highlight Circle() .trim(from: 0, to: 0.3) .stroke(Color.white.opacity(0.5), lineWidth: 2) .frame(width: 40, height: 40) .rotationEffect(.degrees(-45)) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24, height: 24) .foregroundColor(isMissing ? .gray : moodContrastingTextColor) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 6) { Text(cachedWeekdayWide) .font(.body.weight(.semibold)) .foregroundColor(textColor) HStack(spacing: 6) { Text(cachedMonthAbbrevDay) .font(.caption) .foregroundColor(textColor.opacity(0.6)) if !isMissing { // Glass pill for mood Text(entry.moodString) .font(.caption.weight(.medium)) .foregroundColor(moodColor) .padding(.horizontal, 10) .padding(.vertical, 4) .background( Capsule() .fill(.ultraThinMaterial) .overlay( Capsule() .fill(moodColor.opacity(0.15)) ) ) } } } Spacer() // Glass chevron Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(textColor.opacity(0.4)) } .padding(18) // Bottom edge highlight VStack { Spacer() RoundedRectangle(cornerRadius: 24) .fill( LinearGradient( colors: [Color.clear, Color.white.opacity(0.1)], startPoint: .top, endPoint: .bottom ) ) .frame(height: 20) } .clipShape(RoundedRectangle(cornerRadius: 24)) } .frame(height: 88) .overlay( RoundedRectangle(cornerRadius: 24) .stroke( LinearGradient( colors: [ Color.white.opacity(colorScheme == .dark ? 0.3 : 0.6), Color.white.opacity(colorScheme == .dark ? 0.1 : 0.2), Color.white.opacity(colorScheme == .dark ? 0.2 : 0.4) ], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) ) .shadow(color: Color.black.opacity(0.1), radius: 20, x: 0, y: 10) } // MARK: - Motion Style (Accelerometer Parallax) private var motionStyle: some View { MotionCardView( entry: entry, imagePack: imagePack, moodTint: moodTint, theme: theme, colorScheme: colorScheme, isMissing: isMissing, moodColor: moodColor ) } // MARK: - Micro Style (Ultra Compact) private var microStyle: some View { HStack(spacing: 10) { // Tiny mood indicator dot Circle() .fill(isMissing ? Color.gray.opacity(0.4) : moodColor) .frame(width: 8, height: 8) // Compact icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) .foregroundColor(isMissing ? .gray : moodColor) .accessibilityLabel(entry.mood.strValue) // Date - very compact Text(cachedMonthAbbrevDay) .font(.caption.weight(.medium).monospaced()) .foregroundColor(textColor.opacity(0.7)) // Weekday initial Text(String(Random.weekdayName(fromDate: entry.forDate).prefix(3))) .font(.caption2.weight(.semibold)) .foregroundColor(textColor.opacity(0.4)) .frame(width: 28) Spacer() // Mood as tiny pill if !isMissing { Text(entry.moodString) .font(.caption2.weight(.semibold)) .foregroundColor(moodColor) .padding(.horizontal, 8) .padding(.vertical, 3) .background( Capsule() .fill(moodColor.opacity(0.15)) ) } else { Text("tap") .font(.caption2.weight(.medium)) .foregroundColor(.gray.opacity(0.6)) } Image(systemName: "chevron.right") .font(.caption2.weight(.semibold)) .foregroundColor(textColor.opacity(0.25)) } .padding(.horizontal, 14) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 10) .fill(colorScheme == .dark ? Color(.systemGray6).opacity(0.6) : Color.white.opacity(0.8)) ) .overlay( RoundedRectangle(cornerRadius: 10) .stroke( isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2), lineWidth: 0.5 ) ) } // MARK: - Orbit Style (Celestial Circular) private var orbitStyle: some View { OrbitEntryView( entry: entry, imagePack: imagePack, moodColor: moodColor, theme: theme, colorScheme: colorScheme, isMissing: isMissing ) } } // MARK: - Orbit Entry View struct OrbitEntryView: View { let entry: MoodEntryModel let imagePack: MoodImages let moodColor: Color let theme: Theme let colorScheme: ColorScheme let isMissing: Bool private var textColor: Color { theme.currentTheme.labelColor } // Cached date strings private var cachedDay: String { DateFormattingCache.shared.string(for: entry.forDate, format: .day) } private var cachedWeekdayWide: String { DateFormattingCache.shared.string(for: entry.forDate, format: .weekdayWide) } private var cachedMonthAbbrevYear: String { DateFormattingCache.shared.string(for: entry.forDate, format: .monthAbbreviatedYear) } var body: some View { HStack(spacing: 0) { // Orbital system on left orbitalSystem .frame(width: 100, height: 100) // Content on right VStack(alignment: .leading, spacing: 8) { moodDisplay dateDisplay } .padding(.leading, 8) Spacer() chevron } .padding(.horizontal, 16) .padding(.vertical, 12) .background(backgroundLayer) .overlay(borderLayer) } private var orbitalSystem: some View { ZStack { // Orbital ring Circle() .stroke( (colorScheme == .dark ? Color.white : Color.black).opacity(0.1), lineWidth: 1 ) .frame(width: 80, height: 80) // Center core (mood icon) ZStack { // Glow Circle() .fill(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.3)) .frame(width: 60, height: 60) .blur(radius: 8) // Planet Circle() .fill(isMissing ? Color.gray.opacity(0.4) : moodColor) .frame(width: 44, height: 44) .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.5), radius: 6, x: 0, y: 2) // Icon imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 24, height: 24) .foregroundColor(isMissing ? .gray : moodColor.contrastingTextColor) .accessibilityLabel(entry.mood.strValue) } // Orbiting day number orbitingDay } } private var orbitingDay: some View { let angle = -Double.pi / 4 // Position at top-right let radius: CGFloat = 40 return ZStack { Circle() .fill(Color.white.opacity(colorScheme == .dark ? 0.9 : 1.0)) .frame(width: 28, height: 28) .shadow(color: .black.opacity(0.15), radius: 4) Text(cachedDay) .font(.caption.weight(.bold)) .foregroundColor(.black.opacity(0.7)) } .offset(x: cos(angle) * radius, y: sin(angle) * radius) } private var dateDisplay: some View { VStack(alignment: .leading, spacing: 2) { Text(cachedWeekdayWide) .font(.body.weight(.semibold)) .foregroundColor(textColor) Text(cachedMonthAbbrevYear) .font(.caption) .foregroundColor(textColor.opacity(0.5)) } } private var moodDisplay: some View { Group { if isMissing { Text("No mood recorded") .font(.subheadline) .foregroundColor(.gray) } else { Text(entry.moodString) .font(.subheadline.weight(.semibold)) .foregroundColor(moodColor) .padding(.horizontal, 12) .padding(.vertical, 5) .background( Capsule() .fill(moodColor.opacity(0.15)) ) } } } private var chevron: some View { Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(textColor.opacity(0.3)) } private var backgroundLayer: some View { RoundedRectangle(cornerRadius: 20) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) .shadow( color: isMissing ? Color.black.opacity(0.05) : moodColor.opacity(0.15), radius: 12, x: 0, y: 6 ) } private var borderLayer: some View { RoundedRectangle(cornerRadius: 20) .stroke( isMissing ? Color.gray.opacity(0.15) : moodColor.opacity(0.2), lineWidth: 1 ) } } // MARK: - Motion Card with Accelerometer struct MotionCardView: View { let entry: MoodEntryModel let imagePack: MoodImages let moodTint: MoodTints let theme: Theme let colorScheme: ColorScheme let isMissing: Bool let moodColor: Color private var textColor: Color { theme.currentTheme.labelColor } // Cached date string private var cachedDay: String { DateFormattingCache.shared.string(for: entry.forDate, format: .day) } @ObservedObject private var motionManager = MotionManager.shared var body: some View { ZStack { // Background with parallax offset RoundedRectangle(cornerRadius: 22) .fill( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.1), Color.gray.opacity(0.05)] : [moodColor.opacity(0.2), moodColor.opacity(0.05)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .offset( x: motionManager.xOffset * 0.5, y: motionManager.yOffset * 0.5 ) // Floating orbs that move with device GeometryReader { geo in Circle() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.3), Color.clear] : [moodColor.opacity(0.4), Color.clear], center: .center, startRadius: 0, endRadius: 40 ) ) .frame(width: 80, height: 80) .offset( x: geo.size.width * 0.7 + motionManager.xOffset * 2, y: -10 + motionManager.yOffset * 2 ) Circle() .fill( RadialGradient( colors: isMissing ? [Color.gray.opacity(0.2), Color.clear] : [moodColor.opacity(0.3), Color.clear], center: .center, startRadius: 0, endRadius: 30 ) ) .frame(width: 50, height: 50) .offset( x: 20 - motionManager.xOffset * 1.5, y: geo.size.height * 0.6 - motionManager.yOffset * 1.5 ) } // Main content HStack(spacing: 16) { // Icon with enhanced parallax ZStack { // Glow behind icon Circle() .fill(isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.4)) .frame(width: 60, height: 60) .blur(radius: 10) .offset( x: motionManager.xOffset * 0.8, y: motionManager.yOffset * 0.8 ) Circle() .fill( LinearGradient( colors: isMissing ? [Color.gray.opacity(0.5), Color.gray.opacity(0.3)] : [moodColor.opacity(0.9), moodColor.opacity(0.6)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 54, height: 54) .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.4), radius: 8, x: 0, y: 4) imagePack.icon(forMood: entry.mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30, height: 30) .foregroundColor(isMissing ? .gray : moodColor.contrastingTextColor) .offset( x: -motionManager.xOffset * 0.3, y: -motionManager.yOffset * 0.3 ) .accessibilityLabel(entry.mood.strValue) } VStack(alignment: .leading, spacing: 4) { // Day with motion Text(cachedDay) .font(.title.weight(.bold)) .foregroundColor(isMissing ? .gray : moodColor) .offset( x: motionManager.xOffset * 0.2, y: motionManager.yOffset * 0.2 ) HStack(spacing: 6) { Text(Random.weekdayName(fromDate: entry.forDate)) .font(.subheadline.weight(.medium)) .foregroundColor(textColor.opacity(0.7)) if !isMissing { Circle() .fill(moodColor) .frame(width: 4, height: 4) Text(entry.moodString) .font(.subheadline.weight(.semibold)) .foregroundColor(moodColor) } } } Spacer() Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(textColor.opacity(0.3)) .offset( x: motionManager.xOffset * 0.5, y: 0 ) } .padding(.horizontal, 18) .padding(.vertical, 14) } .frame(height: 90) .clipShape(RoundedRectangle(cornerRadius: 22)) .background( RoundedRectangle(cornerRadius: 22) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) .overlay( RoundedRectangle(cornerRadius: 22) .stroke( isMissing ? Color.gray.opacity(0.2) : moodColor.opacity(0.3), lineWidth: 1 ) ) .shadow(color: (isMissing ? Color.gray : moodColor).opacity(0.15), radius: 12, x: 0, y: 6) .onAppear { motionManager.startIfNeeded() } .onDisappear { motionManager.stopIfNoConsumers() } } } // MARK: - Motion Manager for Accelerometer (Shared Singleton) class MotionManager: ObservableObject { /// Shared singleton - avoids creating per-cell instances static let shared = MotionManager() private let motionManager = CMMotionManager() @Published var xOffset: CGFloat = 0 @Published var yOffset: CGFloat = 0 private var isRunning = false private var activeConsumers = 0 private init() {} func startIfNeeded() { activeConsumers += 1 guard !isRunning, motionManager.isDeviceMotionAvailable, !UIAccessibility.isReduceMotionEnabled else { return } isRunning = true motionManager.deviceMotionUpdateInterval = 1/30 // Reduced from 60fps to 30fps motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in guard let motion = motion, error == nil else { return } withAnimation(.interactiveSpring(response: 0.15, dampingFraction: 0.8)) { self?.xOffset = CGFloat(motion.attitude.roll) * 15 self?.yOffset = CGFloat(motion.attitude.pitch) * 15 } } } func stopIfNoConsumers() { activeConsumers = max(0, activeConsumers - 1) guard activeConsumers == 0, isRunning else { return } isRunning = false motionManager.stopDeviceMotionUpdates() xOffset = 0 yOffset = 0 } func stop() { guard isRunning else { return } activeConsumers = 0 isRunning = false motionManager.stopDeviceMotionUpdates() } } struct EntryListView_Previews: PreviewProvider { @MainActor static let fakeData = DataController.shared.randomEntries(count: 1).first! static var previews: some View { VStack(spacing: 8) { EntryListView(entry: EntryListView_Previews.fakeData) EntryListView(entry: EntryListView_Previews.fakeData) } .padding() } }