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

@@ -12,11 +12,6 @@ import WireProtocol
struct TimelineSheet: View {
let viewModel: TimelineViewModel
private enum Metrics {
static let rowSpacing: CGFloat = 10
static let iconColumnWidth: CGFloat = 24
}
var body: some View {
NavigationStack {
content
@@ -66,6 +61,7 @@ struct TimelineSheet: View {
Task { await viewModel.load() }
}
.buttonStyle(.borderedProminent)
.tint(DS.Palette.accent)
}
}
@@ -83,21 +79,25 @@ struct TimelineSheet: View {
}
private func row(_ event: TimelineEvent) -> some View {
HStack(spacing: Metrics.rowSpacing) {
HStack(spacing: DS.Space.md12) {
// Timestamp mono/tabular so HH:mm columns line up (direction).
Text(TimelineRowFormat.timeLabel(atMs: event.at))
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
.font(DS.Typography.mono(.caption))
.foregroundStyle(DS.Palette.textSecondary)
// class glyph + semantic color (frozen mapping; unit-tested).
Text(verbatim: TimelineClassStyle.glyph(for: event.class))
.font(.callout)
.font(DS.Typography.callout)
.foregroundStyle(TimelineClassStyle.color(for: event.class))
.frame(width: Metrics.iconColumnWidth)
.frame(width: DS.Space.xxl24)
// Server-derived phrase untrusted: verbatim (never
// LocalizedStringKey/Markdown) + hard single-line truncation.
Text(verbatim: event.label)
.font(.subheadline)
.font(DS.Typography.callout)
.foregroundStyle(DS.Palette.textPrimary)
.lineLimit(1)
Spacer(minLength: 0)
}
.padding(.vertical, DS.Space.xs2)
.accessibilityElement(children: .combine)
}
}
@@ -125,13 +125,14 @@ enum TimelineClassStyle {
}
static func color(for cls: String) -> Color {
// All from the frozen design system no raw SwiftUI colors (UX finding).
switch cls {
case "tool": return .blue
case "waiting": return .orange
case "done": return .green
case "stuck": return .red
case "user": return .purple
default: return .secondary
case "tool": return DS.Palette.timelineTool
case "waiting": return DS.Palette.statusWaiting
case "done": return DS.Palette.statusWorking
case "stuck": return DS.Palette.statusStuck
case "user": return DS.Palette.timelineUser
default: return DS.Palette.textSecondary
}
}
}