Files
nucleic/Sources/NucleicApp/UltracodeStyle.swift
T

102 lines
4.9 KiB
Swift

import SwiftUI
/// Shared visual language for ultracode (orchestration) mode: the signature purple, a
/// gently-pulsing composer glow, and the small effort-pill label. Kept together so the
/// home chat bar, the session composer, and the effort menus all read identically.
enum UltracodeStyle {
/// SF Symbol that marks ultracode throughout the UI — the same sparkles used for the
/// agent's thinking, so "extra intelligence" reads consistently.
static let symbol = "sparkles"
/// A soft violet gradient used for the glow stroke, hot corner to cool corner.
static func strokeGradient(intensity: Double) -> LinearGradient {
LinearGradient(
colors: [
AppTheme.ultracode.opacity(0.55 + 0.40 * intensity),
AppTheme.ultracode.opacity(0.28 + 0.30 * intensity),
],
startPoint: .topLeading, endPoint: .bottomTrailing)
}
}
/// A cute, restrained purple halo that breathes around a view while ultracode is the active
/// effort. Mirrors `StreakBadge`'s idiom — a single `@State` flag toggled to kick off an
/// `easeInOut(...).repeatForever(autoreverses:)` pulse — so the animation feels native to the
/// app. The halo crests brighter and never collapses to nothing, so even at the trough it
/// reads as "ultracode is on", not a flicker. Honors Reduce Motion: the glow still shows, it
/// just holds steady instead of pulsing.
struct UltracodeGlow: ViewModifier {
/// Whether ultracode is the selected effort. When false the modifier is inert (no glow,
/// no animation), so a plain chat composer is untouched.
var active: Bool
var cornerRadius: CGFloat = 8
@Environment(\.accessibilityReduceMotion) private var reduceMotion
/// Toggled once the view is on screen (and whenever `active` flips), which starts —
/// or, under Reduce Motion, simply settles — the repeating pulse.
@State private var pulsing = false
func body(content: Content) -> some View {
// Animate only while ultracode is on, the pulse has been kicked off, and the user
// hasn't asked to reduce motion; otherwise the halo holds at a calm mid intensity.
let animating = active && pulsing && !reduceMotion
let intensity = animating ? 1.0 : 0.45
return content
.overlay {
if active {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.strokeBorder(UltracodeStyle.strokeGradient(intensity: intensity),
lineWidth: animating ? 1.6 : 1.2)
.shadow(color: AppTheme.ultracode.opacity(animating ? 0.55 : 0.22),
radius: animating ? 9 : 4)
// A wider, fainter outer bloom so the glow feels soft rather than a
// hard ring — the second shadow reads as ambient light around the box.
.shadow(color: AppTheme.ultracode.opacity(animating ? 0.30 : 0),
radius: animating ? 16 : 0)
.animation(
reduceMotion
? .default
: .easeInOut(duration: 1.8).repeatForever(autoreverses: true),
value: pulsing)
.allowsHitTesting(false) // purely decorative — never eats composer taps
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 0.3), value: active)
.onAppear { pulsing = active }
.onChange(of: active) { _, now in pulsing = now }
}
}
extension View {
/// Wrap a composer (or any rounded box) in the ultracode purple glow while `active`.
/// Place it *after* the view's own background so the halo sits around the box.
func ultracodeGlow(active: Bool, cornerRadius: CGFloat = 8) -> some View {
modifier(UltracodeGlow(active: active, cornerRadius: cornerRadius))
}
}
/// The effort-menu trigger label when ultracode is selected: a sparkles glyph + "Ultracode"
/// in the signature purple, replacing the plain "Effort: xhigh" text so the mode is obvious
/// at a glance. The sparkle does a slow twinkle to echo the composer glow (steady under
/// Reduce Motion).
struct UltracodeEffortLabel: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var twinkle = false
var body: some View {
let on = twinkle && !reduceMotion
return HStack(spacing: 4) {
Image(systemName: UltracodeStyle.symbol)
.opacity(on ? 1.0 : 0.7)
.scaleEffect(on ? 1.0 : 0.9)
.animation(
reduceMotion ? .default : .easeInOut(duration: 1.2).repeatForever(autoreverses: true),
value: twinkle)
Text("Ultracode")
}
.foregroundStyle(AppTheme.ultracode)
.onAppear { twinkle = true }
}
}