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