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,9 +5,9 @@
import SwiftUI
/// Two-step onboarding: a welcome screen, then the Screen Time permission
/// request. Onboarding only completes once authorization is approved the
/// app cannot block anything without it.
/// Two-step onboarding using system styling: a welcome screen, then the
/// Screen Time permission request. Onboarding only completes once
/// authorization is approved the app cannot block anything without it.
struct OnboardingView: View {
@Environment(ScreenTimeAuthorization.self) private var authorization
@Environment(\.openURL) private var openURL
@@ -31,23 +31,18 @@ struct OnboardingView: View {
Spacer()
footer
}
.padding(.horizontal, 28)
.padding(.bottom, 24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundStyle(.white)
.background(Theme.background)
.padding()
}
private var welcome: some View {
VStack(spacing: 18) {
VStack(spacing: 16) {
Image(systemName: "scissors")
.font(.system(size: 56, weight: .semibold))
.foregroundStyle(Theme.accent)
.font(.system(size: 56))
.foregroundStyle(.tint)
Text("Severed")
.font(.system(size: 38, weight: .bold))
.font(.largeTitle.bold())
Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.")
.font(.system(size: 16))
.foregroundStyle(Theme.textSecondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
@@ -55,10 +50,10 @@ struct OnboardingView: View {
private var permission: some View {
VStack(spacing: 24) {
Image(systemName: "hourglass")
.font(.system(size: 48, weight: .semibold))
.foregroundStyle(Theme.accent)
.font(.system(size: 48))
.foregroundStyle(.tint)
Text("Allow Screen Time Access")
.font(.system(size: 28, weight: .bold))
.font(.title.bold())
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 14) {
bullet("shield.fill", "Severed uses Apple's Screen Time framework to block the apps you choose.")
@@ -68,8 +63,8 @@ struct OnboardingView: View {
if authorization.status == .denied || authorization.lastRequestFailed {
VStack(spacing: 10) {
Text("Screen Time access was declined. Severed can't block apps without it.")
.font(.system(size: 13))
.foregroundStyle(Theme.destructive)
.font(.footnote)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
.accessibilityIdentifier("permissionDeniedLabel")
Button("Open Settings") {
@@ -77,8 +72,6 @@ struct OnboardingView: View {
openURL(url)
}
}
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(Theme.accent)
.accessibilityIdentifier("openSettingsButton")
}
}
@@ -88,12 +81,11 @@ struct OnboardingView: View {
private func bullet(_ systemImage: String, _ text: String) -> some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: systemImage)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(Theme.accent)
.frame(width: 22)
.foregroundStyle(.tint)
.frame(width: 24)
Text(text)
.font(.system(size: 15))
.foregroundStyle(Theme.textSecondary)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
@@ -127,13 +119,10 @@ struct OnboardingView: View {
) -> some View {
Button(action: action) {
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.black)
.frame(maxWidth: .infinity)
.frame(height: 54)
.background(.white, in: RoundedRectangle(cornerRadius: 27))
}
.buttonStyle(.plain)
.buttonStyle(.borderedProminent)
.controlSize(.large)
.accessibilityIdentifier(identifier)
}
}