Files
Reflect/Shared/Views/EntryListView.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

2294 lines
88 KiB
Swift

//
// 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
}
// 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
}
}
.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 {
return "\(dateString), \(entry.mood.strValue)"
}
}
// 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()
}
}