refactor: re-skin UI with native iOS components

Replace the Opal-style custom presentation with the bare iOS design
language, keeping the backend, flows, and accessibility identifiers:

- Home: NavigationStack + List; rules now presented as list rows with
  kind icon, block summary, and trailing live status; '+' in the toolbar
- New Rule: plain list of rule types and presets; editor still pushed
  via navigationDestination
- Editor: native Form (DatePicker rows, day circles with summary in the
  section header, toggle rows with footers, stepper rows); create
  commits with a prominent 'Add Rule' button replacing Hold to Commit;
  edit uses toolbar Done plus red Disable/Delete rows
- Detail: sheet with inline title + status caption, LabeledContent
  rows; Edit Rule pushes the editor natively
- Default color scheme: drop forced dark mode and custom tint; system
  appearance and accent throughout
- Delete Theme, HoldToCommitButton, RuleCardView and custom chrome
- Use concrete Color.primary/secondary in button row labels (the
  hierarchical styles resolve against the button tint)
- Tests: 93 passing; only delta is holdToCommitButton press →
  commitRuleButton tap in 5 call sites
- Spec: add §6 mapping the original presentation to the native one

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 15:14:01 -04:00
parent c99538565c
commit fb219aa8d8
13 changed files with 525 additions and 914 deletions

View File

@@ -5,29 +5,19 @@
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.
/// Seven circular day toggles (S M T W T F S) using system colors, meant to
/// sit inside a Form/List row. The day-set summary is shown by the enclosing
/// section header.
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)
}
HStack(spacing: 8) {
ForEach(Weekday.displayOrder, id: \.self) { day in
dayToggle(day)
}
}
.padding(16)
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 18))
.padding(.vertical, 4)
}
private func dayToggle(_ day: Weekday) -> some View {
@@ -40,13 +30,16 @@ struct DayOfWeekPicker: View {
}
} label: {
Text(day.shortLabel)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(isOn ? .black : Theme.textSecondary)
.font(.subheadline.weight(.semibold))
.foregroundStyle(isOn ? Color.white : .secondary)
.frame(maxWidth: .infinity)
.aspectRatio(1, contentMode: .fit)
.background(isOn ? Color.white : Theme.surfaceElevated, in: Circle())
.background(
isOn ? AnyShapeStyle(.tint) : AnyShapeStyle(Color(.tertiarySystemFill)),
in: Circle()
)
}
.buttonStyle(.plain)
.buttonStyle(.borderless)
.accessibilityIdentifier("dayToggle-\(day.rawValue)")
.accessibilityLabel(day.abbreviation)
.accessibilityAddTraits(isOn ? .isSelected : [])
@@ -55,7 +48,15 @@ struct DayOfWeekPicker: View {
#Preview {
@Previewable @State var days = Weekday.weekdays
DayOfWeekPicker(days: $days)
.padding()
.background(Theme.background)
Form {
Section {
DayOfWeekPicker(days: $days)
} header: {
HStack {
Text("On these days").textCase(nil)
Spacer()
Text(days.summary).textCase(nil)
}
}
}
}