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

@@ -84,6 +84,8 @@ struct AddMoodHeaderView: View {
AuraVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .orbit:
OrbitVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .neon:
NeonVotingView(moodTint: moodTint, onMoodSelected: addItem)
}
}
@@ -549,3 +551,227 @@ struct AddMoodHeaderView_Previews: PreviewProvider {
}
}
}
// MARK: - Layout 7: Neon (Synthwave Arcade Equalizer)
struct NeonVotingView: View {
let moodTint: MoodTints
let onMoodSelected: (Mood) -> Void
@State private var pulsePhase = false
@State private var hoveredMood: Mood?
// Synthwave color palette
private let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.82)
private let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8)
private let neonYellow = Color(red: 1.0, green: 0.9, blue: 0.0)
private let deepBlack = Color(red: 0.02, green: 0.02, blue: 0.04)
var body: some View {
ZStack {
// Grid background
neonGridBackground
// Equalizer bars
HStack(spacing: 8) {
ForEach(Mood.allValues, id: \.self) { mood in
NeonEqualizerBar(
mood: mood,
moodTint: moodTint,
isHovered: hoveredMood == mood,
pulsePhase: pulsePhase,
neonCyan: neonCyan,
neonMagenta: neonMagenta,
onTap: { onMoodSelected(mood) }
)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
LinearGradient(
colors: [neonCyan.opacity(0.6), neonMagenta.opacity(0.6)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
)
.shadow(color: neonCyan.opacity(0.2), radius: 20, x: 0, y: 0)
.shadow(color: neonMagenta.opacity(0.15), radius: 30, x: 0, y: 10)
.onAppear {
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
pulsePhase = true
}
}
}
private var neonGridBackground: some View {
ZStack {
// Deep black base
deepBlack
// Grid
Canvas { context, size in
let gridSpacing: CGFloat = 20
let cyanColor = Color(red: 0.0, green: 0.8, blue: 0.7)
// Horizontal lines
for y in stride(from: 0, to: 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(cyanColor.opacity(0.08)), lineWidth: 0.5)
}
// Vertical lines
for x in stride(from: 0, to: 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(cyanColor.opacity(0.08)), lineWidth: 0.5)
}
}
// Ambient glow at bottom
LinearGradient(
colors: [
neonMagenta.opacity(0.15),
Color.clear
],
startPoint: .bottom,
endPoint: .top
)
.blur(radius: 30)
}
}
}
struct NeonEqualizerBar: View {
let mood: Mood
let moodTint: MoodTints
let isHovered: Bool
let pulsePhase: Bool
let neonCyan: Color
let neonMagenta: Color
let onTap: () -> Void
@State private var isPressed = false
private var barHeight: CGFloat {
switch mood {
case .great: return 140
case .good: return 115
case .average: return 90
case .bad: return 65
case .horrible: return 45
default: return 90
}
}
private var barColor: Color {
switch mood {
case .great: return neonCyan
case .good: return Color(red: 0.2, green: 1.0, blue: 0.6)
case .average: return Color(red: 1.0, green: 0.9, blue: 0.0)
case .bad: return Color(red: 1.0, green: 0.5, blue: 0.0)
case .horrible: return neonMagenta
default: return Color(red: 1.0, green: 0.9, blue: 0.0)
}
}
var body: some View {
Button(action: onTap) {
VStack(spacing: 8) {
// The equalizer bar
ZStack(alignment: .bottom) {
// Glow background
RoundedRectangle(cornerRadius: 6)
.fill(barColor.opacity(pulsePhase ? 0.15 : 0.08))
.frame(height: barHeight + 20)
.blur(radius: 15)
// Main bar
RoundedRectangle(cornerRadius: 6)
.fill(
LinearGradient(
colors: [
barColor,
barColor.opacity(0.7)
],
startPoint: .top,
endPoint: .bottom
)
)
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
.shadow(color: barColor.opacity(0.8), radius: pulsePhase ? 12 : 8, x: 0, y: 0)
.shadow(color: barColor.opacity(0.4), radius: pulsePhase ? 20 : 15, x: 0, y: 5)
// Top highlight
RoundedRectangle(cornerRadius: 6)
.fill(
LinearGradient(
colors: [Color.white.opacity(0.5), Color.clear],
startPoint: .top,
endPoint: .center
)
)
.frame(height: isPressed ? barHeight * 0.9 : barHeight)
// Level indicators (horizontal lines)
VStack(spacing: 8) {
ForEach(0..<Int(barHeight / 15), id: \.self) { _ in
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(height: 2)
}
}
.frame(height: isPressed ? barHeight * 0.9 - 10 : barHeight - 10)
.clipShape(RoundedRectangle(cornerRadius: 4))
.padding(.horizontal, 4)
.padding(.bottom, 5)
}
.frame(maxHeight: 160, alignment: .bottom)
// Mood label
Text(mood.shortLabel)
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundColor(barColor)
.shadow(color: barColor.opacity(0.8), radius: 4, x: 0, y: 0)
}
}
.buttonStyle(NeonBarButtonStyle(isPressed: $isPressed))
.frame(maxWidth: .infinity)
}
}
struct NeonBarButtonStyle: ButtonStyle {
@Binding var isPressed: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
.onChange(of: configuration.isPressed) { _, newValue in
isPressed = newValue
}
}
}
private extension Mood {
var shortLabel: String {
switch self {
case .great: return "GRT"
case .good: return "GUD"
case .average: return "AVG"
case .bad: return "BAD"
case .horrible: return "HRB"
default: return "---"
}
}
}