Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Package.resolved
.env
.env.local
docs/plans
docs/schema

# macOS
.DS_Store
Expand Down
70 changes: 60 additions & 10 deletions Extensions/SparkControls/Sources/SparkControlsBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,74 @@ import WidgetKit
@main
struct SparkControlsBundle: WidgetBundle {
var body: some Widget {
PlaceholderControl()
QuickCheckInControl()
OpenTodayControl()
FocusDomainControl()
}
}

/// Phase 1 stub. A real control ships in Phase 3.
struct PlaceholderControl: ControlWidget {
// MARK: - Quick Check-In

struct QuickCheckInControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "co.cronx.spark.controls.checkin") {
ControlWidgetButton(action: QuickCheckInAction()) {
Label("Check In", systemImage: "plus.circle.fill")
}
}
.displayName("Quick Check-In")
.description("Log a mood check-in without opening Spark.")
}
}

struct QuickCheckInAction: AppIntent {
static let title: LocalizedStringResource = "Quick Check-In"
static let openAppWhenRun = true

func perform() async throws -> some IntentResult {
// Phase 3 Week 3: drive LogCheckInIntent from SparkIntelligence
.result()
}
}

// MARK: - Open Today

struct OpenTodayControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "co.cronx.spark.controls.placeholder") {
ControlWidgetButton(action: NoopIntent()) {
Label("Spark", systemImage: "sparkles")
StaticControlConfiguration(kind: "co.cronx.spark.controls.open-today") {
ControlWidgetButton(action: OpenTodayAction()) {
Label("Open Spark", systemImage: "sparkles")
}
}
.displayName("Spark")
.description("Placeholder control — real surface lands in Phase 3.")
.displayName("Open Spark")
.description("Open the Spark Today view from Control Center.")
}
}

struct NoopIntent: AppIntent {
static let title: LocalizedStringResource = "No-op"
struct OpenTodayAction: AppIntent {
static let title: LocalizedStringResource = "Open Today"
static let openAppWhenRun = true

func perform() async throws -> some IntentResult { .result() }
}

// MARK: - Focus Domain Toggle

struct FocusDomainControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "co.cronx.spark.controls.focus-domain") {
ControlWidgetButton(action: FocusDomainAction()) {
Label("Focus", systemImage: "scope")
}
}
.displayName("Spark Focus")
.description("Toggle the active focus domain filter in Spark.")
}
}

struct FocusDomainAction: AppIntent {
static let title: LocalizedStringResource = "Toggle Focus Domain"
static let openAppWhenRun = true

func perform() async throws -> some IntentResult { .result() }
}
26 changes: 4 additions & 22 deletions Extensions/SparkIntents/Sources/SparkIntentsBundle.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
import AppIntents

/// Phase 1 stub. Real intents land in Phase 3.
struct PingSparkIntent: AppIntent {
static let title: LocalizedStringResource = "Ping Spark"
static let description = IntentDescription("Placeholder intent to prove the target compiles.")

func perform() async throws -> some IntentResult {
.result()
}
}

struct SparkAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: PingSparkIntent(),
phrases: ["Ping \(.applicationName)"],
shortTitle: "Ping Spark",
systemImageName: "sparkles"
)
}
}
// Importing SparkIntelligence embeds all AppIntents (read + action) and
// SparkShortcuts into this extension's binary, making them discoverable
// by the system for Siri and Shortcuts.
@_exported import SparkIntelligence
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ActivityKit
import SparkKit
import SwiftUI
import WidgetKit

// MARK: - Lock Screen layout

struct RingsLockScreenView: View {
let context: ActivityViewContext<DailyActivityAttributes>

var body: some View {
HStack(spacing: 20) {
// Nested rings (move → exercise → stand)
ZStack {
ring(progress: context.state.moveProgress, color: .red, size: 70, lineWidth: 8)
ring(progress: context.state.exerciseProgress, color: .green, size: 52, lineWidth: 7)
ring(progress: context.state.standProgress, color: .cyan, size: 36, lineWidth: 6)
}

VStack(alignment: .leading, spacing: 6) {
metricRow(icon: "figure.walk", color: .green, label: "\(context.state.stepsDisplay) steps")
metricRow(icon: "flame.fill", color: .red, label: moveLabel)
metricRow(icon: "bolt.fill", color: .cyan, label: standLabel)
}

Spacer()
}
.padding(16)
.containerBackground(for: .widget) {
LinearGradient(
colors: [Color.green.opacity(0.2), Color.teal.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}

private var moveLabel: String {
let pct = Int(context.state.moveProgress * 100)
return "Move \(pct)%"
}

private var standLabel: String {
let pct = Int(context.state.standProgress * 100)
return "Stand \(pct)%"
}

private func ring(progress: Double, color: Color, size: CGFloat, lineWidth: CGFloat) -> some View {
ZStack {
Circle()
.stroke(color.opacity(0.2), lineWidth: lineWidth)
Circle()
.trim(from: 0, to: min(1, max(0, progress)))
.stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.rotationEffect(.degrees(-90))
}
.frame(width: size, height: size)
}

private func metricRow(icon: String, color: Color, label: String) -> some View {
Label(label, systemImage: icon)
.font(.caption.weight(.medium))
.foregroundStyle(color)
.lineLimit(1)
}
}

// MARK: - Dynamic Island compact views

struct RingsIslandCompactLeading: View {
let state: DailyActivityAttributes.DailyContentState
var body: some View {
HStack(spacing: 2) {
miniRing(progress: state.moveProgress, color: .red)
miniRing(progress: state.exerciseProgress, color: .green)
miniRing(progress: state.standProgress, color: .cyan)
}
}
private func miniRing(progress: Double, color: Color) -> some View {
ZStack {
Circle().stroke(color.opacity(0.3), lineWidth: 2.5)
Circle()
.trim(from: 0, to: min(1, max(0, progress)))
.stroke(color, style: StrokeStyle(lineWidth: 2.5, lineCap: .round))
.rotationEffect(.degrees(-90))
}
.frame(width: 14, height: 14)
}
}

struct RingsIslandCompactTrailing: View {
let state: DailyActivityAttributes.DailyContentState
var body: some View {
Text(state.stepsDisplay)
.font(.caption.weight(.bold).monospacedDigit())
.foregroundStyle(.green)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import ActivityKit
import SparkKit
import SwiftUI
import WidgetKit

// MARK: - Lock Screen layout

struct SleepLockScreenView: View {
let context: ActivityViewContext<SleepActivityAttributes>

var body: some View {
HStack(spacing: 16) {
Text(phaseEmoji)
.font(.system(size: 36))

VStack(alignment: .leading, spacing: 4) {
Text(context.state.phaseLabel)
.font(.headline.weight(.semibold))

if let dur = context.state.durationDisplay {
Text(dur)
.font(.subheadline)
.foregroundStyle(.secondary)
} else if let wake = context.attributes.targetWakeTime {
Text("Wake at \(timeString(wake))")
.font(.subheadline)
.foregroundStyle(.secondary)
}

if let score = context.state.sleepScore {
Text("Score: \(score)/100")
.font(.caption.weight(.medium))
.foregroundStyle(.indigo)
}
}

Spacer()
}
.padding(16)
.containerBackground(for: .widget) {
LinearGradient(
colors: [Color.indigo.opacity(0.3), Color.purple.opacity(0.15)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}

private var phaseEmoji: String {
switch context.state.phase {
case .preparing: return "🌙"
case .sleeping: return "😴"
case .wakingUp: return "☀️"
case .resolved: return "✅"
}
}

private func timeString(_ date: Date) -> String {
let f = DateFormatter()
f.timeStyle = .short
f.dateStyle = .none
return f.string(from: date)
}
}

// MARK: - Dynamic Island compact views

struct SleepIslandCompactLeading: View {
let state: SleepActivityAttributes.SleepContentState
var body: some View {
Text(emoji(state.phase))
.font(.caption)
}
private func emoji(_ phase: SleepActivityAttributes.SleepContentState.Phase) -> String {
switch phase {
case .preparing: return "🌙"
case .sleeping: return "😴"
case .wakingUp: return "☀️"
case .resolved: return "✅"
}
}
}

struct SleepIslandCompactTrailing: View {
let state: SleepActivityAttributes.SleepContentState
var body: some View {
if let score = state.sleepScore {
Text("\(score)")
.font(.caption.weight(.bold).monospacedDigit())
.foregroundStyle(.indigo)
} else if let dur = state.durationDisplay {
Text(dur)
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
}
}
}
Loading
Loading