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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user