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:
@@ -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 "---"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user