fix: editor usability and visual polish from UI scan
- 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 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,15 @@ struct RuleDraft: Hashable {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ struct DayOfWeekPicker: View {
|
||||
@Binding var days: Set<Weekday>
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Weekday.displayOrder, id: \.self) { day in
|
||||
dayToggle(day)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private func dayToggle(_ day: Weekday) -> some View {
|
||||
@@ -31,13 +30,16 @@ struct DayOfWeekPicker: View {
|
||||
} label: {
|
||||
Text(day.shortLabel)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(isOn ? Color.white : .secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.foregroundStyle(isOn ? Color.white : Color.secondary)
|
||||
.frame(width: 38, height: 38)
|
||||
.background(
|
||||
isOn ? AnyShapeStyle(.tint) : AnyShapeStyle(Color(.tertiarySystemFill)),
|
||||
in: Circle()
|
||||
)
|
||||
// Each cell takes an equal share of the row and at least a
|
||||
// 44pt-tall hit area, so the whole strip is comfortably tappable.
|
||||
.frame(maxWidth: .infinity, minHeight: 44)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.accessibilityIdentifier("dayToggle-\(day.rawValue)")
|
||||
|
||||
@@ -21,12 +21,11 @@ struct RuleEditorView: View {
|
||||
var onToggleEnabled: (() -> Void)?
|
||||
var onDelete: (() -> Void)?
|
||||
|
||||
@State private var showingRename = false
|
||||
@State private var showingAppPicker = false
|
||||
@State private var renameText = ""
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
nameSection
|
||||
sections
|
||||
if case .edit(let isEnabled) = mode {
|
||||
Section {
|
||||
@@ -46,22 +45,15 @@ struct RuleEditorView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(draft.name)
|
||||
Text(draft.sanitized().name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.accessibilityIdentifier("ruleEditorTitle")
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Rename", systemImage: "pencil") {
|
||||
renameText = draft.name
|
||||
showingRename = true
|
||||
}
|
||||
.accessibilityIdentifier("renameButton")
|
||||
}
|
||||
if case .edit = mode {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
onCommit(draft)
|
||||
onCommit(draft.sanitized())
|
||||
}
|
||||
.accessibilityIdentifier("doneButton")
|
||||
}
|
||||
@@ -70,28 +62,22 @@ struct RuleEditorView: View {
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if mode == .create {
|
||||
Button {
|
||||
onCommit(draft)
|
||||
onCommit(draft.sanitized())
|
||||
} label: {
|
||||
Text("Add Rule")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 8)
|
||||
.accessibilityIdentifier("commitRuleButton")
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 10)
|
||||
.frame(maxWidth: .infinity)
|
||||
// Bar material so scrolling form content doesn't collide
|
||||
// with the floating button.
|
||||
.background(.bar)
|
||||
}
|
||||
}
|
||||
.alert("Rule Name", isPresented: $showingRename) {
|
||||
TextField("Name", text: $renameText)
|
||||
Button("OK") {
|
||||
let trimmed = renameText.trimmingCharacters(in: .whitespaces)
|
||||
if !trimmed.isEmpty {
|
||||
draft.name = trimmed
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
.sheet(isPresented: $showingAppPicker) {
|
||||
AppSelectionSheet(draft: $draft)
|
||||
}
|
||||
@@ -99,6 +85,16 @@ struct RuleEditorView: View {
|
||||
|
||||
// MARK: - Sections
|
||||
|
||||
private var nameSection: some View {
|
||||
Section {
|
||||
TextField("Rule Name", text: $draft.name)
|
||||
.submitLabel(.done)
|
||||
.accessibilityIdentifier("ruleNameField")
|
||||
} header: {
|
||||
Text("Name").textCase(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var sections: some View {
|
||||
switch draft.kind {
|
||||
|
||||
Reference in New Issue
Block a user