- Rule name is now an inline text field at the top of the editor; the pencil/rename button and alert are gone (it was unclear what the edit button did). Names are sanitized on commit: trimmed, falling back to the kind's default when emptied - Day-of-week toggles fill the full row width with equal cells and >= 44pt tap targets - 'Add Rule' commit button sits on a bar-material bottom inset so form content no longer collides with it while scrolling (found via computer-use scan; verified clean in a follow-up scan) - Tests: 95 passing — new specs for the inline rename flow, day-toggle geometry, and name sanitization; three tests now scroll to reach bottom-of-form controls, matching real user behavior - Spec §6 updated for the editor changes
99 lines
3.0 KiB
Swift
99 lines
3.0 KiB
Swift
//
|
||
// RuleDraft.swift
|
||
// Severed
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// Value-type working copy of a rule used by the editors, so cancelling an
|
||
/// edit never touches the persisted model. Hashable so it can drive
|
||
/// `navigationDestination(item:)`.
|
||
struct RuleDraft: Hashable {
|
||
var name: String
|
||
var kind: RuleKind
|
||
var days: Set<Weekday>
|
||
var startMinutes: Int
|
||
var endMinutes: Int
|
||
var dailyLimitMinutes: Int
|
||
var maxOpens: Int
|
||
var hardMode: Bool
|
||
var blockAdultContent: Bool
|
||
var selectionMode: SelectionMode
|
||
var selectionData: Data?
|
||
var selectionCount: Int
|
||
|
||
/// A fresh draft for a new rule of the given kind, using the reference
|
||
/// app's defaults (9–5 weekdays schedule, 45m/day, 5 opens/day).
|
||
init(kind: RuleKind) {
|
||
self.name = kind.defaultRuleName
|
||
self.kind = kind
|
||
self.days = Weekday.weekdays
|
||
self.startMinutes = 9 * 60
|
||
self.endMinutes = 17 * 60
|
||
self.dailyLimitMinutes = 45
|
||
self.maxOpens = 5
|
||
self.hardMode = false
|
||
self.blockAdultContent = false
|
||
self.selectionMode = .block
|
||
self.selectionData = nil
|
||
self.selectionCount = 0
|
||
}
|
||
|
||
init(rule: BlockingRule) {
|
||
self.name = rule.name
|
||
self.kind = rule.kind
|
||
self.days = rule.days
|
||
self.startMinutes = rule.startMinutes
|
||
self.endMinutes = rule.endMinutes
|
||
self.dailyLimitMinutes = rule.dailyLimitMinutes
|
||
self.maxOpens = rule.maxOpens
|
||
self.hardMode = rule.hardMode
|
||
self.blockAdultContent = rule.blockAdultContent
|
||
self.selectionMode = rule.selectionMode
|
||
self.selectionData = rule.selectionData
|
||
self.selectionCount = rule.selectionCount
|
||
}
|
||
|
||
init(preset: RulePreset) {
|
||
self.init(kind: .schedule)
|
||
self.name = preset.name
|
||
self.days = preset.days
|
||
self.startMinutes = preset.startMinutes
|
||
self.endMinutes = preset.endMinutes
|
||
}
|
||
|
||
func apply(to rule: BlockingRule) {
|
||
rule.name = name
|
||
rule.kind = kind
|
||
rule.days = days
|
||
rule.startMinutes = startMinutes
|
||
rule.endMinutes = endMinutes
|
||
rule.dailyLimitMinutes = dailyLimitMinutes
|
||
rule.maxOpens = maxOpens
|
||
rule.hardMode = hardMode
|
||
rule.blockAdultContent = blockAdultContent
|
||
rule.selectionMode = selectionMode
|
||
rule.selectionData = selectionData
|
||
rule.selectionCount = selectionCount
|
||
}
|
||
|
||
func makeRule() -> BlockingRule {
|
||
let rule = BlockingRule(name: name, kind: kind)
|
||
apply(to: rule)
|
||
return rule
|
||
}
|
||
|
||
/// Trims the name and falls back to the kind's default when it is empty,
|
||
/// so a cleared name field can never produce an unnamed rule.
|
||
func sanitized() -> RuleDraft {
|
||
var copy = self
|
||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
||
copy.name = trimmed.isEmpty ? kind.defaultRuleName : trimmed
|
||
return copy
|
||
}
|
||
|
||
var schedule: RuleSchedule {
|
||
RuleSchedule(startMinutes: startMinutes, endMinutes: endMinutes, days: days)
|
||
}
|
||
}
|