From af919c1585d886258812b1006f4e7ba16f2f4392 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 12 Jun 2026 16:29:32 -0400 Subject: [PATCH] refactor: move rule editor actions into the navigation bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Both editor modes commit via a checkmark confirmation button (role: .confirm) in the navigation bar — accessibility labels 'Add Rule' (create) and 'Done' (edit); replaces the bottom 'Add Rule' button, so nothing floats over the form anymore - Edit mode gains an ellipsis 'Rule Actions' menu next to the checkmark holding Disable/Enable Rule and destructive Delete Rule; the red form rows are removed - Tests pin the checkmark to the navigation bar with its label and drive disable/delete through the menu; the swipe-to-reach workarounds are gone. 95 passing - Spec §6 updated --- Severed/Views/Rules/RuleEditorView.swift | 73 ++++++++++------------ SeveredUITests/RuleCreationUITests.swift | 7 ++- SeveredUITests/RuleManagementUITests.swift | 18 +++--- docs/RULES_FEATURE_SPEC.md | 2 +- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Severed/Views/Rules/RuleEditorView.swift b/Severed/Views/Rules/RuleEditorView.swift index 6a82575..13331db 100644 --- a/Severed/Views/Rules/RuleEditorView.swift +++ b/Severed/Views/Rules/RuleEditorView.swift @@ -6,9 +6,9 @@ import SwiftUI /// The rule editor as a plain Form, always pushed inside a NavigationStack -/// (New Rule flow and detail editing both push it). Creation commits with a -/// prominent "Add Rule" button; editing uses a toolbar Done plus red -/// Disable/Delete rows. +/// (New Rule flow and detail editing both push it). Both modes commit via a +/// checkmark confirmation button in the navigation bar; edit mode adds an +/// ellipsis "Rule Actions" menu (Disable/Enable, Delete) next to it. struct RuleEditorView: View { enum Mode: Equatable { case create @@ -27,20 +27,6 @@ struct RuleEditorView: View { Form { nameSection sections - if case .edit(let isEnabled) = mode { - Section { - Button(isEnabled ? "Disable Rule" : "Enable Rule") { - onToggleEnabled?() - } - .foregroundStyle(.red) - .accessibilityIdentifier("toggleEnabledButton") - - Button("Delete Rule", role: .destructive) { - onDelete?() - } - .accessibilityIdentifier("deleteRuleButton") - } - } } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -50,32 +36,41 @@ struct RuleEditorView: View { .lineLimit(1) .accessibilityIdentifier("ruleEditorTitle") } - if case .edit = mode { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - onCommit(draft.sanitized()) + if case .edit(let isEnabled) = mode { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button(isEnabled ? "Disable Rule" : "Enable Rule") { + onToggleEnabled?() + } + Button("Delete Rule", role: .destructive) { + onDelete?() + } + } label: { + Image(systemName: "ellipsis") } - .accessibilityIdentifier("doneButton") + .accessibilityLabel("Rule Actions") + .accessibilityIdentifier("ruleActionsMenu") } } - } - .safeAreaInset(edge: .bottom) { - if mode == .create { - Button { - onCommit(draft.sanitized()) - } label: { - Text("Add Rule") - .frame(maxWidth: .infinity) + ToolbarItem(placement: .confirmationAction) { + switch mode { + case .create: + Button(role: .confirm) { + onCommit(draft.sanitized()) + } label: { + Image(systemName: "checkmark") + } + .accessibilityLabel("Add Rule") + .accessibilityIdentifier("commitRuleButton") + case .edit: + Button(role: .confirm) { + onCommit(draft.sanitized()) + } label: { + Image(systemName: "checkmark") + } + .accessibilityLabel("Done") + .accessibilityIdentifier("doneButton") } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .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) } } .sheet(isPresented: $showingAppPicker) { diff --git a/SeveredUITests/RuleCreationUITests.swift b/SeveredUITests/RuleCreationUITests.swift index a5d271e..15c0c8b 100644 --- a/SeveredUITests/RuleCreationUITests.swift +++ b/SeveredUITests/RuleCreationUITests.swift @@ -22,8 +22,11 @@ final class RuleCreationUITests: XCTestCase { XCTAssertEqual(app.staticTexts["ruleEditorTitle"].waitToAppear().label, "In the Zone") XCTAssertTrue(app.staticTexts["During this time"].exists) - // Hold to commit saves the rule and returns home. - app.buttons["commitRuleButton"].waitToAppear().tap() + // The confirmation checkmark lives in the navigation bar and carries + // a descriptive accessibility label. + let commit = app.navigationBars.buttons["commitRuleButton"].waitToAppear() + XCTAssertEqual(commit.label, "Add Rule") + commit.tap() app.buttons["ruleCard-In the Zone"].waitToAppear() XCTAssertFalse(app.element("emptyRulesCard").exists) } diff --git a/SeveredUITests/RuleManagementUITests.swift b/SeveredUITests/RuleManagementUITests.swift index 54c2a7f..f6577bf 100644 --- a/SeveredUITests/RuleManagementUITests.swift +++ b/SeveredUITests/RuleManagementUITests.swift @@ -47,10 +47,12 @@ final class RuleManagementUITests: XCTestCase { app.buttons["ruleCard-Sleep"].waitToAppear().tap() app.buttons["editRuleButton"].waitToAppear().tap() - // The disable/delete rows sit at the bottom of the form. - app.staticTexts["ruleEditorTitle"].waitToAppear() - app.swipeUp() - app.buttons["toggleEnabledButton"].waitToAppear().tap() + + // Disable lives in the ellipsis menu in the navigation bar. + let actionsMenu = app.navigationBars.buttons["ruleActionsMenu"].waitToAppear() + XCTAssertEqual(actionsMenu.label, "Rule Actions") + actionsMenu.tap() + app.buttons["Disable Rule"].waitToAppear().tap() // The detail caption now reports the rule as disabled. let status = app.staticTexts["detailStatusLabel"].waitToAppear() @@ -66,10 +68,10 @@ final class RuleManagementUITests: XCTestCase { app.buttons["ruleCard-Sleep"].waitToAppear().tap() app.buttons["editRuleButton"].waitToAppear().tap() - // The disable/delete rows sit at the bottom of the form. - app.staticTexts["ruleEditorTitle"].waitToAppear() - app.swipeUp() - app.buttons["deleteRuleButton"].waitToAppear().tap() + + // Delete lives in the ellipsis menu in the navigation bar. + app.navigationBars.buttons["ruleActionsMenu"].waitToAppear().tap() + app.buttons["Delete Rule"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear() XCTAssertFalse( diff --git a/docs/RULES_FEATURE_SPEC.md b/docs/RULES_FEATURE_SPEC.md index bb1566d..51f693e 100644 --- a/docs/RULES_FEATURE_SPEC.md +++ b/docs/RULES_FEATURE_SPEC.md @@ -344,7 +344,7 @@ reference for *what* the feature does; presentation now maps as follows: | Apps home | `NavigationStack` + `List`; "Blocked Apps" and "Rules" sections; **rules are list rows** (kind icon, name, block summary, trailing live status — green when active); "+" toolbar button | | Rule detail | Sheet with inline nav title (name + "Schedule, 6h left" caption), `LabeledContent` rows, "Edit Rule" row pushes the editor; hard-locked rules show a lock row instead | | New Rule | `List` with a "Rule Type" section and preset sections as plain rows; editor pushed via `navigationDestination(item:)` | -| Rule editor | Native `Form`: an inline **Name text field** at the top (no separate rename button; empty names fall back to the kind default), `DatePicker` rows, full-width day-circle row (≥44pt tap targets) with the summary in the section header, toggle rows with footers, stepper rows. Create commits with a prominent **"Add Rule"** button on a bar-material inset (replaces Hold to Commit); edit uses toolbar **Done** plus red Disable/Delete rows | +| Rule editor | Native `Form`: an inline **Name text field** at the top (no separate rename button; empty names fall back to the kind default), `DatePicker` rows, full-width day-circle row (≥44pt tap targets) with the summary in the section header, toggle rows with footers, stepper rows. Both modes commit via a **checkmark** in the navigation bar (labels: "Add Rule" / "Done"; replaces Hold to Commit). In edit mode an **ellipsis menu** ("Rule Actions") next to the checkmark holds Disable Rule and the destructive Delete Rule | | Onboarding / app picker | System styling, `.borderedProminent` buttons, default color scheme (no forced dark, default accent) | Dropped custom components: `Theme`, `HoldToCommitButton`, `RuleCardView`,