Add Neon/Synthwave style and 4 paywall themes

- Add 4 distinct paywall themes (Celestial, Garden, Neon, Minimal) with
  preview/switcher in debug settings
- Add Neon voting layout with synthwave equalizer bar design
- Upgrade Neon entry style with grid background, cyan/magenta gradients,
  scanline effects, and mini equalizer visualization
- Add PaywallPreviewSettingsView for testing different paywall styles
- Use consistent synthwave color palette (cyan #00FFD0, magenta #FF00CC)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-26 23:05:45 -06:00
parent f45f52ccbf
commit 53eb953b77
9 changed files with 2144 additions and 117 deletions

View File

@@ -590,103 +590,205 @@ struct EntryListView: View {
.background(colorScheme == .dark ? Color(.systemGray6) : .white)
}
// MARK: - Neon Style (Cyberpunk/Synthwave)
// MARK: - Neon Style (Synthwave Arcade)
private var neonStyle: some View {
ZStack {
// Dark base with scanline effect
RoundedRectangle(cornerRadius: 4)
.fill(Color.black)
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)
// Scanline overlay
VStack(spacing: 2) {
ForEach(0..<20, id: \.self) { _ in
Rectangle()
.fill(Color.white.opacity(0.03))
.frame(height: 1)
Spacer().frame(height: 3)
// 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: 4))
.clipShape(RoundedRectangle(cornerRadius: 6))
// Scanline overlay for CRT effect
VStack(spacing: 0) {
ForEach(0..<30, id: \.self) { _ in
Rectangle()
.fill(Color.white.opacity(0.015))
.frame(height: 1)
Spacer().frame(height: 2)
}
}
.clipShape(RoundedRectangle(cornerRadius: 6))
// Content
HStack(spacing: 16) {
// Neon-outlined mood indicator
// Neon equalizer-style mood indicator
ZStack {
// Glow effect
RoundedRectangle(cornerRadius: 8)
.stroke(isMissing ? Color.gray : moodColor, lineWidth: 2)
// 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)
.opacity(0.8)
RoundedRectangle(cornerRadius: 8)
.stroke(isMissing ? Color.gray : moodColor, lineWidth: 2)
// 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 : moodColor)
.shadow(color: isMissing ? .clear : moodColor, radius: 8, x: 0, y: 0)
.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: 52, height: 52)
.frame(width: 54, height: 54)
VStack(alignment: .leading, spacing: 6) {
// Date in monospace terminal style
VStack(alignment: .leading, spacing: 5) {
// Date in cyan monospace
Text(entry.forDate, format: .dateTime.year().month(.twoDigits).day(.twoDigits))
.font(.caption.weight(.medium).monospaced())
.foregroundColor(Color(red: 0.4, green: 1.0, blue: 0.4)) // Terminal green
.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(.headline.weight(.bold).monospaced())
.foregroundColor(.gray)
.font(.system(.headline, design: .monospaced).weight(.black))
.foregroundColor(.gray.opacity(0.6))
} else {
// Mood in glowing text
// Mood text with synthwave gradient glow
Text(entry.moodString.uppercased())
.font(.headline.weight(.black))
.foregroundColor(moodColor)
.shadow(color: moodColor.opacity(0.8), radius: 6, x: 0, y: 0)
.shadow(color: moodColor.opacity(0.4), radius: 12, x: 0, y: 0)
.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
// Weekday in magenta
Text(entry.forDate, format: .dateTime.weekday(.wide))
.font(.caption2.weight(.medium).monospaced())
.foregroundColor(.white.opacity(0.4))
.font(.system(.caption2, design: .monospaced).weight(.medium))
.foregroundColor(neonMagenta.opacity(0.7))
.textCase(.uppercase)
}
Spacer()
// Chevron with glow
// 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))
.foregroundColor(isMissing ? .gray : moodColor)
.shadow(color: isMissing ? .clear : moodColor, radius: 4, x: 0, y: 0)
.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, 16)
.padding(.vertical, 14)
// Neon border
RoundedRectangle(cornerRadius: 4)
// Cyan-to-magenta gradient border
RoundedRectangle(cornerRadius: 6)
.stroke(
LinearGradient(
colors: isMissing
? [Color.gray.opacity(0.3), Color.gray.opacity(0.1)]
: [moodColor.opacity(0.8), moodColor.opacity(0.3)],
? [Color.gray.opacity(0.2), Color.gray.opacity(0.1)]
: [neonCyan.opacity(0.7), neonMagenta.opacity(0.7)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
}
.shadow(
color: isMissing ? .clear : moodColor.opacity(0.3),
radius: 12,
x: 0,
y: 4
)
// 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)