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.
This commit is contained in:
61
Severed/Views/Components/DayOfWeekPicker.swift
Normal file
61
Severed/Views/Components/DayOfWeekPicker.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// DayOfWeekPicker.swift
|
||||
// Severed
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// "On these days:" — seven circular toggles (S M T W T F S) with a
|
||||
/// human-readable summary, as in the reference rule editors.
|
||||
struct DayOfWeekPicker: View {
|
||||
@Binding var days: Set<Weekday>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("On these days:")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
Spacer()
|
||||
Text(days.summary)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(Theme.textSecondary)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Weekday.displayOrder, id: \.self) { day in
|
||||
dayToggle(day)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 18))
|
||||
}
|
||||
|
||||
private func dayToggle(_ day: Weekday) -> some View {
|
||||
let isOn = days.contains(day)
|
||||
return Button {
|
||||
if isOn {
|
||||
days.remove(day)
|
||||
} else {
|
||||
days.insert(day)
|
||||
}
|
||||
} label: {
|
||||
Text(day.shortLabel)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(isOn ? .black : Theme.textSecondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.background(isOn ? Color.white : Theme.surfaceElevated, in: Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("dayToggle-\(day.rawValue)")
|
||||
.accessibilityLabel(day.abbreviation)
|
||||
.accessibilityAddTraits(isOn ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var days = Weekday.weekdays
|
||||
DayOfWeekPicker(days: $days)
|
||||
.padding()
|
||||
.background(Theme.background)
|
||||
}
|
||||
Reference in New Issue
Block a user