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:
2026-06-12 15:44:25 -04:00
parent 2ca365e3f4
commit 1868d788f9
7 changed files with 86 additions and 36 deletions

View File

@@ -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)")

View File

@@ -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 {