Rebuild the app around recurring screen-time blocking rules modeled on Opal's My Apps tab: - Onboarding with FamilyControls Screen Time authorization - Apps home with Blocked Apps tiles and live rule cards - New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery - Rule editors: time windows (incl. overnight), day picker, app selection via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit - Hard Mode: active hard rules cannot be edited, disabled, deleted, or unblocked until their window ends; soft rules pause until next window - Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer - 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg harness for in-memory storage, mocked authorization, seeded scenarios - docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording Known gap: background window transitions and time/open-limit thresholds need a DeviceActivityMonitor extension; shields currently sync while the app is running.
74 lines
2.5 KiB
Swift
74 lines
2.5 KiB
Swift
//
|
||
// Theme.swift
|
||
// Severed
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// Dark, green-tinted palette matching the reference app.
|
||
enum Theme {
|
||
static let background = Color(red: 0.03, green: 0.05, blue: 0.04)
|
||
static let surface = Color.white.opacity(0.07)
|
||
static let surfaceElevated = Color.white.opacity(0.12)
|
||
static let accent = Color(red: 0.66, green: 0.88, blue: 0.50)
|
||
static let activeCardTop = Color(red: 0.10, green: 0.20, blue: 0.12)
|
||
static let activeCardBottom = Color(red: 0.05, green: 0.10, blue: 0.06)
|
||
static let textSecondary = Color.white.opacity(0.6)
|
||
static let textTertiary = Color.white.opacity(0.4)
|
||
static let destructive = Color(red: 1.0, green: 0.32, blue: 0.36)
|
||
|
||
static let commitGradient = LinearGradient(
|
||
colors: [
|
||
Color(red: 0.35, green: 0.65, blue: 0.85),
|
||
Color(red: 0.45, green: 0.75, blue: 0.55),
|
||
Color(red: 0.85, green: 0.80, blue: 0.45),
|
||
],
|
||
startPoint: .leading, endPoint: .trailing
|
||
)
|
||
}
|
||
|
||
/// Circular chrome button used in sheet headers (✕, ‹, ✎).
|
||
struct CircleIconButton: View {
|
||
let systemImage: String
|
||
var tint: Color = .white
|
||
let action: () -> Void
|
||
|
||
var body: some View {
|
||
Button(action: action) {
|
||
Image(systemName: systemImage)
|
||
.font(.system(size: 15, weight: .semibold))
|
||
.foregroundStyle(tint)
|
||
.frame(width: 38, height: 38)
|
||
.background(Theme.surfaceElevated, in: Circle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
|
||
/// The "calendar → shield" icon pair shown on rule cards, details, and presets.
|
||
struct RuleIconPair: View {
|
||
let kind: RuleKind
|
||
var isActive = false
|
||
|
||
var body: some View {
|
||
HStack(spacing: 8) {
|
||
iconTile(systemImage: kind.symbolName, tinted: isActive)
|
||
Image(systemName: "arrow.right")
|
||
.font(.system(size: 11, weight: .bold))
|
||
.foregroundStyle(Theme.textTertiary)
|
||
iconTile(systemImage: "shield.fill", tinted: isActive)
|
||
}
|
||
}
|
||
|
||
private func iconTile(systemImage: String, tinted: Bool) -> some View {
|
||
Image(systemName: systemImage)
|
||
.font(.system(size: 14, weight: .semibold))
|
||
.foregroundStyle(tinted ? Theme.accent : .white.opacity(0.7))
|
||
.frame(width: 32, height: 32)
|
||
.background(
|
||
(tinted ? Theme.accent.opacity(0.18) : Theme.surfaceElevated),
|
||
in: RoundedRectangle(cornerRadius: 9)
|
||
)
|
||
}
|
||
}
|