Files
web-terminal/ios/App/WebTerm/Wiring/RootView.swift
Yaojia Wang 660a40491a feat(ios): comprehensive iPhone+iPad UX polish — refined-native design system
Freeze a shared design system (DesignSystem/{Tokens,Typography,StatusStyle,
Primitives}.swift): indigo #7C8CFF accent (root .tint), semantic status colors,
2-4-8-12-16-20-24 spacing + sm8/md12/lg16 radii scale, SF Mono tabular numbers,
reduce-motion-gated animations, haptics, reusable StatusBadge/TelemetryChip/Card/
SectionHeader/DSButtonStyle/ContinueLastBanner. Every changed view consumes tokens
— no hardcoded colors/spacing.

Applied across all surfaces (visual only — zero behavior/logic change, all suites
green): session list rows + status system (shape+color, not color-alone) +
telemetry chips + thumbnail placeholders; terminal gate card (≥44pt approve/reject)
+ keybar + reconnect + quick-reply + digest + SwiftTerm accent theme; pairing hero
+ warning tiers + Projects cards + Timeline/Diff; nav chrome + iPad split placeholder
+ privacy shade + motion. Chinese gate copy. UX finding fixed: timeline class colors
now via DS.Palette (+timelineTool/timelineUser tokens).

Design review 8.5/10. Verified: iPhone 16 290 + iPad Pro 11 290 tests green;
packages + integration green; consistency audit ~clean; zero changes under
ios/Packages, src/, public/.
2026-07-05 22:00:31 +02:00

178 lines
7.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import HostRegistry
import SwiftUI
/// T-iOS-15 / T-iPad-2 · App `NavigationStack` +
/// + sheets/scenePhase/deepLink 线iPad T-iPad-2
///
/// - `RootView` struct `@main` WebTermApp
/// `RootView(coordinator:)` `AdaptiveRootView`
/// accent tint`DS.Palette.accent`DS
/// App //`.borderedProminent`
/// - `AdaptiveRootView` `horizontalSizeClass` `StackRootView`compact
/// `SplitRootView`regulariPad
/// / scenePhase / deepLink / sheets **线**
/// ZStack
///
/// `StackRootView` `RootView` body **** compact
/// iPhone
/// `DS.*` token //
struct RootView: View {
@Bindable var coordinator: AppCoordinator
var body: some View {
AdaptiveRootView(coordinator: coordinator)
// DS tint Tokens.swift
// gate amber orange `.tint`
.tint(DS.Palette.accent)
}
}
/// T-iPad-2 · compact iPhone / iPad Slide Over
/// `RootView` `NavigationStack` 线
/// /scenePhase/deepLink/sheets `AdaptiveRootView`
struct StackRootView: View {
@Bindable var coordinator: AppCoordinator
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
NavigationStack {
rootContent
.navigationDestination(isPresented: terminalBinding) {
terminalDestination
}
}
}
// MARK: - Route switch
@ViewBuilder private var rootContent: some View {
switch coordinator.route {
case .loading:
ProgressView()
case .pairing:
firstRunPairing
case .sessions:
sessionList
}
}
@ViewBuilder private var firstRunPairing: some View {
if let viewModel = coordinator.rootPairingViewModel {
PairingScreen(viewModel: viewModel) { host in
coordinator.completeFirstPairing(host)
}
} else {
ProgressView()
}
}
private var sessionList: some View {
SessionListScreen(
viewModel: coordinator.sessionList,
onOpen: { coordinator.open($0) },
onAddHost: { coordinator.presentAddHost() }
)
.safeAreaInset(edge: .bottom) { continueLastBanner }
// / DS reduceMotion
.animation(
DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion),
value: coordinator.continueLastSessionId
)
// T-iOS-26 · Projects RootView SessionListScreen
// T-iOS-23 W7 leading
// topBarTrailing hostMenu
.toolbar { ProjectsToolbarItem(coordinator: coordinator) }
}
// MARK: - "" highlight (cold start step 5)
@ViewBuilder private var continueLastBanner: some View {
if coordinator.continueLastSessionId != nil {
ContinueLastBanner { coordinator.openContinueLast() }
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
// MARK: - Terminal push
private var terminalBinding: Binding<Bool> {
Binding(
get: { coordinator.terminalController != nil },
set: { presented in
guard !presented else { return }
coordinator.closeTerminal() // back engine.close() (detach)
}
)
}
@ViewBuilder private var terminalDestination: some View {
if let controller = coordinator.terminalController {
TerminalContainerView(
controller: controller,
onNewSessionInCwd: { coordinator.openNewSessionInCurrentCwd() },
onKillSession: { coordinator.killCurrentSession() } // T-iPad-3
)
// T-iOS-29 · identity PER CONTROLLER: an in-place session switch
// (new-in-cwd, deep link) swaps the controller while the
// destination stays presented without a new identity SwiftUI
// keeps the old SwiftTerm UIView and the new ViewModel's output
// sink never attaches (makeUIView never re-runs). Also resets the
// container's per-session @State (plan-gate dismissal, timeline).
.id(controller.id)
}
}
}
// MARK: - Continue-last banner (shared stack + split, DRY)
/// 5 CTA stack split sidebar
/// DRY`.regularMaterial` + +
/// accent `DSButtonStyle(.primary)`
/// `action`= `coordinator.openContinueLast()`
struct ContinueLastBanner: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label(RootCopy.continueLast, systemImage: "arrow.uturn.forward.circle.fill")
}
.buttonStyle(DSButtonStyle(kind: .primary))
.accessibilityIdentifier("root.continueLastButton")
.padding(.horizontal, DS.Space.lg16)
.padding(.top, DS.Space.md12)
.padding(.bottom, DS.Space.sm8)
.background(.regularMaterial)
.overlay(alignment: .top) {
Rectangle()
.fill(DS.Palette.hairline)
.frame(height: DS.Stroke.hairline)
}
}
}
// MARK: - Projects toolbar item (shared stack + split, DRY)
/// leading stack split disabled
/// `presentProjects` a11y id tint label accent
struct ProjectsToolbarItem: ToolbarContent {
@Bindable var coordinator: AppCoordinator
var body: some ToolbarContent {
ToolbarItem(placement: .topBarLeading) {
Button {
coordinator.presentProjects()
} label: {
Label(RootCopy.projects, systemImage: "folder")
}
.disabled(coordinator.sessionList.activeHost == nil)
.accessibilityIdentifier("sessions.projectsButton")
}
}
}
/// internal `SplitRootView` /DRY
enum RootCopy {
static let continueLast = "继续上次会话"
static let projects = "项目"
}