Add Aura voting layout and 12 new day view styles
New voting layout style with atmospheric glowing orb design. New day view styles: Aura, Chronicle, Neon, Ink, Prism, Tape, Morph, Stack, Wave, Pattern, Leather, and Glass. Updated style pickers to use horizontal ScrollView for better navigation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
"WebSearch",
|
||||
"WebFetch(domain:swiftwithmajid.com)",
|
||||
"WebFetch(domain:azamsharp.com)",
|
||||
"WebFetch(domain:www.createwithswift.com)"
|
||||
"WebFetch(domain:www.createwithswift.com)",
|
||||
"Skill(frontend-design:frontend-design)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ enum VotingLayoutStyle: Int, CaseIterable {
|
||||
case cards = 1 // Larger tappable cards with labels
|
||||
case radial = 2 // Semi-circle/wheel arrangement
|
||||
case stacked = 3 // Full-width vertical list
|
||||
case aura = 4 // Atmospheric glowing orbs with flowing layout
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
@@ -19,6 +20,7 @@ enum VotingLayoutStyle: Int, CaseIterable {
|
||||
case .cards: return "Cards"
|
||||
case .radial: return "Radial"
|
||||
case .stacked: return "Stacked"
|
||||
case .aura: return "Aura"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +31,18 @@ enum DayViewStyle: Int, CaseIterable {
|
||||
case compact = 2 // Dense timeline view
|
||||
case bubble = 3 // Colorful full-width bubbles
|
||||
case grid = 4 // 3 entries per row grid
|
||||
case aura = 5 // Atmospheric glowing entries with giant typography
|
||||
case chronicle = 6 // Editorial magazine with dramatic serif typography
|
||||
case neon = 7 // Cyberpunk synthwave with glowing edges
|
||||
case ink = 8 // Japanese zen calligraphy with brush strokes
|
||||
case prism = 9 // Premium glassmorphism with light refraction
|
||||
case tape = 10 // Retro cassette mixtape aesthetic
|
||||
case morph = 11 // Liquid organic blob shapes
|
||||
case stack = 12 // Layered paper notes with depth
|
||||
case wave = 13 // Horizontal gradient river bands
|
||||
case pattern = 14 // Mood icons as repeating background pattern
|
||||
case leather = 15 // Skeuomorphic leather with stitching
|
||||
case glass = 16 // iOS 26 liquid glass with variable blur
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
@@ -37,6 +51,18 @@ enum DayViewStyle: Int, CaseIterable {
|
||||
case .compact: return "Compact"
|
||||
case .bubble: return "Bubble"
|
||||
case .grid: return "Grid"
|
||||
case .aura: return "Aura"
|
||||
case .chronicle: return "Chronicle"
|
||||
case .neon: return "Neon"
|
||||
case .ink: return "Ink"
|
||||
case .prism: return "Prism"
|
||||
case .tape: return "Tape"
|
||||
case .morph: return "Morph"
|
||||
case .stack: return "Stack"
|
||||
case .wave: return "Wave"
|
||||
case .pattern: return "Pattern"
|
||||
case .leather: return "Leather"
|
||||
case .glass: return "Glass"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ struct AddMoodHeaderView: View {
|
||||
RadialVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .stacked:
|
||||
StackedVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .aura:
|
||||
AuraVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +233,106 @@ struct StackedVotingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout 5: Aura (Atmospheric glowing orbs)
|
||||
struct AuraVotingView: View {
|
||||
let moodTint: MoodTints
|
||||
let onMoodSelected: (Mood) -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
// Top row: 3 moods (Horrible, Bad, Average)
|
||||
HStack(spacing: 16) {
|
||||
ForEach(Array(Mood.allValues.prefix(3))) { mood in
|
||||
auraButton(for: mood)
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom row: 2 moods (Good, Great) - centered
|
||||
HStack(spacing: 24) {
|
||||
ForEach(Array(Mood.allValues.suffix(2))) { mood in
|
||||
auraButton(for: mood)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
private func auraButton(for mood: Mood) -> some View {
|
||||
let color = moodTint.color(forMood: mood)
|
||||
|
||||
return Button(action: { onMoodSelected(mood) }) {
|
||||
VStack(spacing: 10) {
|
||||
// Glowing orb
|
||||
ZStack {
|
||||
// Outer atmospheric glow
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.5),
|
||||
color.opacity(0.2),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 45
|
||||
)
|
||||
)
|
||||
.frame(width: 90, height: 90)
|
||||
|
||||
// Middle glow ring
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
color.opacity(0.8),
|
||||
color.opacity(0.4)
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 10,
|
||||
endRadius: 30
|
||||
)
|
||||
)
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
// Inner solid core
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 48, height: 48)
|
||||
.shadow(color: color.opacity(0.8), radius: 12, x: 0, y: 0)
|
||||
|
||||
// Icon
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 26, height: 26)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// Label with elegant typography
|
||||
Text(mood.strValue)
|
||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(color)
|
||||
.tracking(0.5)
|
||||
}
|
||||
}
|
||||
.buttonStyle(AuraButtonStyle(color: color))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom button style for aura with glow effect on press
|
||||
struct AuraButtonStyle: ButtonStyle {
|
||||
let color: Color
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.92 : 1.0)
|
||||
.brightness(configuration.isPressed ? 0.1 : 0)
|
||||
.animation(.easeInOut(duration: 0.15), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button Styles
|
||||
struct MoodButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
|
||||
@@ -476,34 +476,37 @@ struct VotingLayoutPickerCompact: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4))
|
||||
|
||||
Text(layout.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||
Text(layout.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(currentLayout == layout
|
||||
? Color.accentColor.opacity(0.1)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(currentLayout == layout
|
||||
? Color.accentColor.opacity(0.1)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +533,34 @@ struct VotingLayoutPickerCompact: View {
|
||||
VStack(spacing: 4) {
|
||||
ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) }
|
||||
}
|
||||
case .aura:
|
||||
// Glowing orbs in 2 rows
|
||||
VStack(spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(0..<3, id: \.self) { _ in
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
||||
.frame(width: 14, height: 14)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack(spacing: 10) {
|
||||
ForEach(0..<2, id: \.self) { _ in
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
||||
.frame(width: 14, height: 14)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,36 +861,39 @@ struct DayViewStylePickerCompact: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
dayViewStyle = style
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
styleIcon(for: style)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
dayViewStyle = style
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
styleIcon(for: style)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4))
|
||||
|
||||
Text(style.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
||||
Text(style.displayName)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5))
|
||||
}
|
||||
.frame(width: 70)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(dayViewStyle == style
|
||||
? Color.accentColor.opacity(0.1)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(dayViewStyle == style
|
||||
? Color.accentColor.opacity(0.1)
|
||||
: (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)))
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,6 +953,201 @@ struct DayViewStylePickerCompact: View {
|
||||
Circle().fill(.green).frame(width: 10, height: 10)
|
||||
Circle().fill(.yellow).frame(width: 10, height: 10)
|
||||
}
|
||||
case .aura:
|
||||
// Giant number with glowing orb
|
||||
HStack(spacing: 4) {
|
||||
Text("17")
|
||||
.font(.system(size: 20, weight: .black, design: .rounded))
|
||||
.foregroundStyle(
|
||||
LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom)
|
||||
)
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(colors: [.green.opacity(0.6), .clear], center: .center, startRadius: 0, endRadius: 12)
|
||||
)
|
||||
.frame(width: 24, height: 24)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
}
|
||||
case .chronicle:
|
||||
// Editorial magazine style
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Rectangle().frame(width: 34, height: 2)
|
||||
HStack(spacing: 4) {
|
||||
Text("12")
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
Rectangle().frame(width: 1, height: 20)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3)
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 8, height: 2).opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .neon:
|
||||
// Cyberpunk neon style
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(Color.black)
|
||||
.frame(width: 38, height: 28)
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(Color.green, lineWidth: 1)
|
||||
.frame(width: 16, height: 16)
|
||||
.shadow(color: .green, radius: 4, x: 0, y: 0)
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.stroke(Color.green.opacity(0.5), lineWidth: 0.5)
|
||||
.frame(width: 38, height: 28)
|
||||
}
|
||||
case .ink:
|
||||
// Japanese zen style
|
||||
HStack(spacing: 6) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.85)
|
||||
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
||||
.frame(width: 18, height: 18)
|
||||
.rotationEffect(.degrees(20))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 14, height: 2).opacity(0.3)
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 10, height: 2).opacity(0.6)
|
||||
}
|
||||
}
|
||||
case .prism:
|
||||
// Glassmorphism with rainbow edge
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(
|
||||
AngularGradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .red], center: .center)
|
||||
)
|
||||
.frame(width: 36, height: 26)
|
||||
.blur(radius: 3)
|
||||
.opacity(0.6)
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.fill(.ultraThinMaterial)
|
||||
.frame(width: 32, height: 22)
|
||||
Circle()
|
||||
.fill(.green.opacity(0.5))
|
||||
.frame(width: 10, height: 10)
|
||||
.offset(x: -6)
|
||||
}
|
||||
case .tape:
|
||||
// Cassette tape reels
|
||||
HStack(spacing: 8) {
|
||||
ZStack {
|
||||
Circle().stroke(lineWidth: 2).frame(width: 14, height: 14)
|
||||
Circle().frame(width: 6, height: 6)
|
||||
}
|
||||
VStack(spacing: 2) {
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 16, height: 3)
|
||||
RoundedRectangle(cornerRadius: 1).frame(width: 16, height: 2).opacity(0.5)
|
||||
}
|
||||
ZStack {
|
||||
Circle().stroke(lineWidth: 2).frame(width: 14, height: 14)
|
||||
Circle().frame(width: 6, height: 6)
|
||||
}
|
||||
}
|
||||
case .morph:
|
||||
// Organic blob shapes
|
||||
ZStack {
|
||||
Ellipse()
|
||||
.fill(.green.opacity(0.4))
|
||||
.frame(width: 28, height: 22)
|
||||
.blur(radius: 4)
|
||||
Ellipse()
|
||||
.fill(.green.opacity(0.6))
|
||||
.frame(width: 18, height: 14)
|
||||
.offset(x: 4, y: 2)
|
||||
.blur(radius: 2)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
case .stack:
|
||||
// Layered paper notes
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.frame(width: 28, height: 22)
|
||||
.opacity(0.3)
|
||||
.offset(x: 3, y: 3)
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.frame(width: 28, height: 22)
|
||||
.opacity(0.5)
|
||||
.offset(x: 1.5, y: 1.5)
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.frame(width: 28, height: 22)
|
||||
VStack(spacing: 3) {
|
||||
Rectangle().frame(width: 18, height: 2)
|
||||
Rectangle().frame(width: 14, height: 2).opacity(0.5)
|
||||
}
|
||||
}
|
||||
case .wave:
|
||||
// Horizontal gradient wave
|
||||
VStack(spacing: 3) {
|
||||
Capsule().fill(.green).frame(width: 34, height: 8)
|
||||
Capsule().fill(.green.opacity(0.6)).frame(width: 34, height: 8)
|
||||
Capsule().fill(.green.opacity(0.3)).frame(width: 34, height: 8)
|
||||
}
|
||||
case .pattern:
|
||||
// Repeating pattern of icons
|
||||
ZStack {
|
||||
VStack(spacing: 6) {
|
||||
HStack(spacing: 8) {
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
Circle().frame(width: 6, height: 6).opacity(0.2)
|
||||
}
|
||||
}
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(.green.opacity(0.3))
|
||||
.frame(width: 28, height: 18)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 12, height: 12)
|
||||
.offset(x: -6)
|
||||
}
|
||||
case .leather:
|
||||
// Skeuomorphic leather
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color(red: 0.4, green: 0.28, blue: 0.18))
|
||||
.frame(width: 36, height: 26)
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [2, 2]))
|
||||
.foregroundColor(Color(red: 0.6, green: 0.5, blue: 0.35))
|
||||
.frame(width: 30, height: 20)
|
||||
Circle()
|
||||
.fill(Color(red: 0.8, green: 0.7, blue: 0.5))
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
case .glass:
|
||||
// Liquid glass effect
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.ultraThinMaterial)
|
||||
.frame(width: 36, height: 26)
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.white.opacity(0.5), .white.opacity(0.1)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 36, height: 26)
|
||||
Circle()
|
||||
.fill(.green.opacity(0.5))
|
||||
.frame(width: 12, height: 12)
|
||||
.offset(x: -6)
|
||||
.blur(radius: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,38 +27,40 @@ struct VotingLayoutPickerView: View {
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
votingLayoutStyle = layout.rawValue
|
||||
}
|
||||
EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName])
|
||||
}) {
|
||||
VStack(spacing: 6) {
|
||||
layoutIcon(for: layout)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6))
|
||||
|
||||
Text(layout.displayName)
|
||||
.font(.caption)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8))
|
||||
Text(layout.displayName)
|
||||
.font(.caption)
|
||||
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8))
|
||||
}
|
||||
.frame(width: 70)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2)
|
||||
)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
@@ -98,6 +100,34 @@ struct VotingLayoutPickerView: View {
|
||||
.frame(width: 32, height: 6)
|
||||
}
|
||||
}
|
||||
case .aura:
|
||||
// Glowing orbs in 2 rows
|
||||
VStack(spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(0..<3, id: \.self) { _ in
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
||||
.frame(width: 14, height: 14)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack(spacing: 10) {
|
||||
ForEach(0..<2, id: \.self) { _ in
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8))
|
||||
.frame(width: 14, height: 14)
|
||||
Circle()
|
||||
.fill(.green)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ struct DayView: View {
|
||||
ForEach(months.sorted(by: {
|
||||
$0.key > $1.key
|
||||
}), id: \.key) { month, entries in
|
||||
Section(header: SectionHeaderView(month: month, year: year)) {
|
||||
Section(header: SectionHeaderView(month: month, year: year, entries: entries)) {
|
||||
monthListView(month: month, year: year, entries: entries)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,40 @@ struct DayView: View {
|
||||
|
||||
// view that make up the list body
|
||||
extension DayView {
|
||||
private func SectionHeaderView(month: Int, year: Int) -> some View {
|
||||
private func SectionHeaderView(month: Int, year: Int, entries: [MoodEntryModel]) -> some View {
|
||||
Group {
|
||||
switch dayViewStyle {
|
||||
case .aura:
|
||||
auraSectionHeader(month: month, year: year)
|
||||
case .chronicle:
|
||||
chronicleSectionHeader(month: month, year: year)
|
||||
case .neon:
|
||||
neonSectionHeader(month: month, year: year)
|
||||
case .ink:
|
||||
inkSectionHeader(month: month, year: year)
|
||||
case .prism:
|
||||
prismSectionHeader(month: month, year: year)
|
||||
case .tape:
|
||||
tapeSectionHeader(month: month, year: year)
|
||||
case .morph:
|
||||
morphSectionHeader(month: month, year: year)
|
||||
case .stack:
|
||||
stackSectionHeader(month: month, year: year)
|
||||
case .wave:
|
||||
waveSectionHeader(month: month, year: year, entries: entries)
|
||||
case .pattern:
|
||||
patternSectionHeader(month: month, year: year)
|
||||
case .leather:
|
||||
leatherSectionHeader(month: month, year: year)
|
||||
case .glass:
|
||||
glassSectionHeader(month: month, year: year)
|
||||
default:
|
||||
defaultSectionHeader(month: month, year: year)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func defaultSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
// Calendar icon
|
||||
Image(systemName: "calendar")
|
||||
@@ -157,6 +190,600 @@ extension DayView {
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func auraSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
// Large month number as hero element
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 48, weight: .black, design: .rounded))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [textColor, textColor.opacity(0.4)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: 80)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.tracking(3)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative element
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.1))
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 20)
|
||||
.background(
|
||||
ZStack {
|
||||
// Base material
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial)
|
||||
|
||||
// Subtle gradient accent
|
||||
LinearGradient(
|
||||
colors: [
|
||||
textColor.opacity(0.03),
|
||||
Color.clear
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func chronicleSectionHeader(month: Int, year: Int) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Thick editorial rule
|
||||
Rectangle()
|
||||
.fill(textColor)
|
||||
.frame(height: 4)
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
// Large serif month name
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 28, weight: .regular, design: .serif))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
// Year in lighter weight
|
||||
Text(String(year))
|
||||
.font(.system(size: 16, weight: .light, design: .serif))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative flourish
|
||||
Text("§")
|
||||
.font(.system(size: 20, weight: .regular, design: .serif))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 16)
|
||||
|
||||
// Thin bottom rule
|
||||
Rectangle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func neonSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
// Glowing terminal prompt
|
||||
Text(">")
|
||||
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4))
|
||||
.shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4).opacity(0.8), radius: 4, x: 0, y: 0)
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased())_\(String(year))")
|
||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .white.opacity(0.3), radius: 2, x: 0, y: 0)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Blinking cursor effect
|
||||
Rectangle()
|
||||
.fill(Color(red: 0.4, green: 1.0, blue: 0.4))
|
||||
.frame(width: 10, height: 18)
|
||||
.shadow(color: Color(red: 0.4, green: 1.0, blue: 0.4), radius: 4, x: 0, y: 0)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
ZStack {
|
||||
Color.black
|
||||
|
||||
// Scanlines
|
||||
VStack(spacing: 3) {
|
||||
ForEach(0..<8, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.02))
|
||||
.frame(height: 1)
|
||||
Spacer().frame(height: 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func inkSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
// Brush stroke accent
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.15))
|
||||
.frame(width: 40, height: 3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .thin))
|
||||
.tracking(4)
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 11, weight: .ultraLight))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Zen circle ornament
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(textColor.opacity(0.2), style: StrokeStyle(lineWidth: 2, lineCap: .round))
|
||||
.frame(width: 20, height: 20)
|
||||
.rotationEffect(.degrees(-60))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 20)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
}
|
||||
|
||||
private func prismSectionHeader(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
// Rainbow edge glow
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(
|
||||
AngularGradient(
|
||||
colors: [.red, .orange, .yellow, .green, .blue, .purple, .red],
|
||||
center: .leading
|
||||
)
|
||||
)
|
||||
.blur(radius: 8)
|
||||
.opacity(0.3)
|
||||
|
||||
// Glass content
|
||||
HStack(spacing: 12) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.frame(width: 4, height: 4)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}
|
||||
|
||||
private func tapeSectionHeader(month: Int, year: Int) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
// Tape reel icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(textColor.opacity(0.3), lineWidth: 2)
|
||||
.frame(width: 24, height: 24)
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("SIDE A")
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
Text("\(Random.monthName(fromMonthInt: month).uppercased()) '\(String(year).suffix(2))")
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.foregroundColor(textColor)
|
||||
.tracking(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Track counter
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 20, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
}
|
||||
|
||||
private func morphSectionHeader(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
// Organic blob background
|
||||
HStack {
|
||||
Ellipse()
|
||||
.fill(textColor.opacity(0.08))
|
||||
.frame(width: 120, height: 60)
|
||||
.blur(radius: 15)
|
||||
.offset(x: -20)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 22, weight: .light))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
|
||||
Spacer()
|
||||
|
||||
// Blob indicator
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.15))
|
||||
.frame(width: 12, height: 12)
|
||||
.blur(radius: 2)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 18)
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func stackSectionHeader(month: Int, year: Int) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Torn edge
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<30, id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(textColor.opacity(0.2))
|
||||
.frame(height: CGFloat.random(in: 2...4))
|
||||
}
|
||||
}
|
||||
.frame(height: 4)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
// Red margin line
|
||||
Rectangle()
|
||||
.fill(Color.red.opacity(0.3))
|
||||
.frame(width: 2)
|
||||
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 14, weight: .light, design: .serif))
|
||||
.italic()
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func waveSectionHeader(month: Int, year: Int, entries: [MoodEntryModel]) -> some View {
|
||||
// Calculate average mood (excluding missing entries)
|
||||
let validEntries = entries.filter { $0.moodValue != Mood.missing.rawValue && $0.moodValue <= 4 }
|
||||
let averageMood: Double = validEntries.isEmpty ? 0 : Double(validEntries.map { $0.moodValue }.reduce(0, +)) / Double(validEntries.count)
|
||||
let hasData = !validEntries.isEmpty
|
||||
|
||||
// Map average to a mood for coloring (0-4 scale)
|
||||
let moodForColor: Mood = {
|
||||
if !hasData { return .missing }
|
||||
switch Int(round(averageMood)) {
|
||||
case 0: return .horrible
|
||||
case 1: return .bad
|
||||
case 2: return .average
|
||||
case 3: return .good
|
||||
default: return .great
|
||||
}
|
||||
}()
|
||||
|
||||
let barColor = hasData ? moodTint.color(forMood: moodForColor) : textColor.opacity(0.2)
|
||||
// Width percentage based on average (0=20%, 4=100%)
|
||||
let widthPercent = hasData ? 0.2 + (averageMood / 4.0) * 0.8 : 0.2
|
||||
|
||||
return HStack(spacing: 0) {
|
||||
// Month number
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 32, weight: .thin))
|
||||
.foregroundColor(hasData ? barColor.opacity(0.6) : textColor.opacity(0.3))
|
||||
.frame(width: 50)
|
||||
|
||||
// Gradient bar sized by average mood
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
Capsule()
|
||||
.fill(textColor.opacity(0.1))
|
||||
.frame(height: 8)
|
||||
|
||||
// Colored bar based on average
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [barColor, barColor.opacity(0.6)],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.frame(width: geo.size.width * widthPercent, height: 8)
|
||||
.shadow(color: barColor.opacity(0.4), radius: 4, x: 0, y: 2)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if hasData {
|
||||
Text(String(format: "%.1f avg", averageMood + 1)) // Display as 1-5
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundColor(barColor)
|
||||
} else {
|
||||
Text(String(year))
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.foregroundColor(textColor.opacity(0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 16)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func patternSectionHeader(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
// Subtle mood icon pattern background
|
||||
GeometryReader { geo in
|
||||
let iconSize: CGFloat = 12
|
||||
let spacing: CGFloat = 20
|
||||
let cols = Int(geo.size.width / spacing) + 1
|
||||
let rows = 3
|
||||
|
||||
Canvas { context, size in
|
||||
for row in 0..<rows {
|
||||
for col in 0..<cols {
|
||||
let x = CGFloat(col) * spacing + (row % 2 == 0 ? 0 : spacing / 2)
|
||||
let y = CGFloat(row) * spacing + 8
|
||||
let rect = CGRect(x: x, y: y, width: iconSize, height: iconSize)
|
||||
context.opacity = 0.06
|
||||
context.fill(Circle().path(in: rect), with: .color(textColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 60)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
// Mood icon cluster
|
||||
HStack(spacing: -4) {
|
||||
ForEach(0..<3, id: \.self) { i in
|
||||
Circle()
|
||||
.fill(textColor.opacity(0.15 - Double(i) * 0.04))
|
||||
.frame(width: 20 - CGFloat(i * 2), height: 20 - CGFloat(i * 2))
|
||||
}
|
||||
}
|
||||
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
private func leatherSectionHeader(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
// Leather texture background
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.45, green: 0.28, blue: 0.18),
|
||||
Color(red: 0.38, green: 0.22, blue: 0.12),
|
||||
Color(red: 0.42, green: 0.25, blue: 0.15)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
|
||||
// Leather grain texture
|
||||
Rectangle()
|
||||
.fill(
|
||||
EllipticalGradient(
|
||||
colors: [.white.opacity(0.08), .clear],
|
||||
center: .topLeading,
|
||||
startRadiusFraction: 0,
|
||||
endRadiusFraction: 0.5
|
||||
)
|
||||
)
|
||||
|
||||
HStack {
|
||||
// Left stitching
|
||||
VStack(spacing: 6) {
|
||||
ForEach(0..<4, id: \.self) { _ in
|
||||
Capsule()
|
||||
.fill(Color(red: 0.7, green: 0.55, blue: 0.35))
|
||||
.frame(width: 6, height: 2)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 16, weight: .bold, design: .serif))
|
||||
.foregroundColor(Color(red: 0.9, green: 0.85, blue: 0.75))
|
||||
.shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 12, weight: .medium, design: .serif))
|
||||
.foregroundColor(Color(red: 0.8, green: 0.7, blue: 0.55))
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Metal rivet
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
Color(red: 0.85, green: 0.75, blue: 0.5),
|
||||
Color(red: 0.55, green: 0.45, blue: 0.25)
|
||||
],
|
||||
center: .topLeading,
|
||||
startRadius: 0,
|
||||
endRadius: 10
|
||||
)
|
||||
)
|
||||
.frame(width: 16, height: 16)
|
||||
Circle()
|
||||
.fill(Color(red: 0.4, green: 0.3, blue: 0.2))
|
||||
.frame(width: 6, height: 6)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.frame(height: 56)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
|
||||
private func glassSectionHeader(month: Int, year: Int) -> some View {
|
||||
ZStack {
|
||||
// Variable blur glass layers
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(.ultraThinMaterial)
|
||||
|
||||
// Light refraction effect
|
||||
GeometryReader { geo in
|
||||
Ellipse()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.white.opacity(0.4), .white.opacity(0)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: geo.size.width * 0.6, height: 30)
|
||||
.blur(radius: 10)
|
||||
.offset(x: 20, y: 5)
|
||||
}
|
||||
|
||||
// Rainbow edge shimmer
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(
|
||||
AngularGradient(
|
||||
colors: [
|
||||
.white.opacity(0.5),
|
||||
.blue.opacity(0.2),
|
||||
.purple.opacity(0.2),
|
||||
.white.opacity(0.3),
|
||||
.white.opacity(0.5)
|
||||
],
|
||||
center: .center
|
||||
),
|
||||
lineWidth: 1
|
||||
)
|
||||
.blur(radius: 0.5)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
// Floating glass orb
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.frame(width: 36, height: 36)
|
||||
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [.white.opacity(0.6), .clear],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 34, height: 34)
|
||||
.blur(radius: 4)
|
||||
|
||||
Text(String(format: "%02d", month))
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text(String(year))
|
||||
.font(.system(size: 12, weight: .regular))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Specular highlight dot
|
||||
Circle()
|
||||
.fill(.white.opacity(0.6))
|
||||
.frame(width: 6, height: 6)
|
||||
.blur(radius: 1)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.frame(height: 64)
|
||||
}
|
||||
|
||||
private var gridColumns: [GridItem] {
|
||||
[
|
||||
GridItem(.flexible(), spacing: 10),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user