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/.
This commit is contained in:
Yaojia Wang
2026-07-05 22:00:31 +02:00
parent 823432b1c8
commit 660a40491a
25 changed files with 1742 additions and 650 deletions

View File

@@ -37,10 +37,8 @@ struct TerminalScreen: View {
/// T-iPad-3 · KeyBar nil =
@State private var keyBarUserOverride: Bool?
private enum Metrics {
static let bannerHorizontalPadding: CGFloat = 12
static let bannerTopPadding: CGFloat = 8
}
/// DS.Motion.gated
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private enum Copy {
static let newSessionInCwd = "在当前目录开新会话"
@@ -67,12 +65,15 @@ struct TerminalScreen: View {
.overlay(alignment: .top) {
if let model = viewModel.bannerModel {
ReconnectBanner(model: model, onNewSession: onNewSessionInCwd)
.padding(.horizontal, Metrics.bannerHorizontalPadding)
.padding(.top, Metrics.bannerTopPadding)
.padding(.horizontal, DS.Space.md12)
.padding(.top, DS.Space.sm8)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
.animation(.default, value: viewModel.bannerModel)
.animation(
DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion),
value: viewModel.bannerModel
)
.toolbar {
newSessionToolbarItem
keyBarToggleToolbarItem
@@ -122,6 +123,27 @@ struct TerminalScreen: View {
}
}
// MARK: - Terminal theme
/// Refined dark terminal theme (). The canvas follows the app surface
/// near-black in dark mode, so the terminal reads as part of the chrome while
/// the caret and selection use the one accent indigo. Every value routes through
/// `DS` (no literals); the glyph font is Dynamic-Type-aware SF Mono so terminal
/// text respects the user's text-size choice at launch.
private enum TerminalTheme {
@MainActor
static func apply(to terminal: TerminalView) {
terminal.font = UIFont.monospacedSystemFont(
ofSize: UIFont.preferredFont(forTextStyle: .footnote).pointSize,
weight: .regular
)
terminal.nativeBackgroundColor = UIColor(DS.Palette.surface)
terminal.nativeForegroundColor = UIColor(DS.Palette.textPrimary)
terminal.caretColor = DS.Palette.accentUIColor()
terminal.selectedTextBackgroundColor = DS.Palette.accentUIColor()
}
}
// MARK: - SwiftTerm bridge
/// `UIViewRepresentable` around `SwiftTerm.TerminalView` (plan §3.5). The
@@ -142,6 +164,7 @@ private struct TerminalHostView: UIViewRepresentable {
func makeUIView(context: Context) -> KeyCommandTerminalView {
let terminal = KeyCommandTerminalView(frame: .zero)
terminal.terminalDelegate = context.coordinator
TerminalTheme.apply(to: terminal)
let viewModel = viewModel
terminal.onKeyCommand = { key in viewModel.send(key: key) }