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
This commit is contained in:
2026-06-12 15:14:01 -04:00
parent fd1f5d758f
commit 2d609043f3
13 changed files with 525 additions and 914 deletions

View File

@@ -6,10 +6,8 @@
import FamilyControls
import SwiftUI
/// App/category/website selection for a rule. Wraps Apple's
/// `FamilyActivityPicker` (the system-sanctioned way to choose apps without
/// seeing their identities) and adds the reference app's Block / Allow Only
/// mode switch.
/// App/category/website selection wrapping Apple's `FamilyActivityPicker`,
/// presented as a plain sheet with a segmented Block / Allow Only control.
struct AppSelectionSheet: View {
@Binding var draft: RuleDraft
@Environment(\.dismiss) private var dismiss
@@ -26,58 +24,56 @@ struct AppSelectionSheet: View {
}
var body: some View {
VStack(spacing: 0) {
HStack {
CircleIconButton(systemImage: "chevron.left") { dismiss() }
NavigationStack {
VStack(spacing: 0) {
Picker("Mode", selection: $mode) {
ForEach(SelectionMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
.padding(.vertical, 8)
.accessibilityIdentifier("selectionModePicker")
FamilyActivityPicker(selection: $selection)
}
.navigationTitle("Selected")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
.accessibilityIdentifier("selectionBackButton")
Spacer()
CircleIconButton(systemImage: "checkmark", tint: .black) { save() }
.background(Theme.accent, in: Circle())
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
save()
}
.accessibilityIdentifier("confirmSelectionButton")
}
.overlay(
Text("Selected")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
)
.padding(.horizontal, 20)
.padding(.top, 18)
Picker("Mode", selection: $mode) {
ForEach(SelectionMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.pickerStyle(.segmented)
.padding(.horizontal, 20)
.padding(.top, 16)
.accessibilityIdentifier("selectionModePicker")
FamilyActivityPicker(selection: $selection)
.padding(.top, 4)
VStack(spacing: 10) {
Text(selectionSummary)
.font(.system(size: 14))
.foregroundStyle(Theme.textSecondary)
.accessibilityIdentifier("selectionCountLabel")
Button {
save()
} label: {
Text("Save")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.black)
.frame(maxWidth: .infinity)
.frame(height: 54)
.background(.white, in: RoundedRectangle(cornerRadius: 27))
.safeAreaInset(edge: .bottom) {
VStack(spacing: 8) {
Text(selectionSummary)
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityIdentifier("selectionCountLabel")
Button {
save()
} label: {
Text("Save")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.accessibilityIdentifier("saveSelectionButton")
}
.buttonStyle(.plain)
.accessibilityIdentifier("saveSelectionButton")
.padding(.horizontal)
.padding(.bottom, 8)
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
}
.background(Theme.background)
}
private var selectionSummary: String {