Files
OpenAppLock/Severed/Views/Theme.swift
Brendan Chen e6c87baeba feat: clone Opal's rules feature with hard block enforcement
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.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 12:35:52 -04:00

74 lines
2.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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)
)
}
}