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

@@ -0,0 +1,86 @@
import SwiftUI
import WireProtocol
/// # Status visuals the SINGLE source (FROZEN public surface)
///
/// Every place that shows a session's Claude-Code status (list rows, badges,
/// thumbnails, project-detail rows, banners) resolves it through here, so the
/// color + shape + label stay identical everywhere. Status is expressed as
/// **color AND a distinct SF Symbol** never color alone (accessibility;
/// color-blind users read the shape). Labels are Chinese for VoiceOver + UI.
/// The seven visual states a status indicator can show. A superset of the wire
/// `ClaudeStatus` (working/waiting/idle/unknown/stuck) plus two App-layer
/// emphasis states:
/// - `pendingApproval` a tool/plan gate is held server-side ("needs me").
/// Outranks status; mirrors `SessionListViewModel.Indicator.pendingApproval`
/// (this type does NOT re-implement that priority callers decide when to
/// use it; here it's only its visual identity).
/// - `exited` the session is over (read-only).
enum DisplayStatus: CaseIterable, Sendable, Equatable {
case working
case waiting
case idle
case stuck
case unknown
case pendingApproval
case exited
/// Bridge from the wire enum. Pure no pending/exited emphasis (callers
/// supply those explicitly, matching the VM's own indicator priority).
init(_ claude: ClaudeStatus) {
switch claude {
case .working: self = .working
case .waiting: self = .waiting
case .idle: self = .idle
case .stuck: self = .stuck
case .unknown: self = .unknown
}
}
}
/// Resolved visuals for one status: a semantic color, a DISTINCT SF Symbol
/// shape, and a Chinese label (used as the VoiceOver string too). The mapping
/// itself is pure no UIKit, deterministic, unit-testable.
struct StatusStyle: Equatable, Sendable {
/// Semantic color from `DS.Palette` (never the sole signal see `symbolName`).
let color: Color
/// SF Symbol name. Distinct across all seven statuses so shape alone
/// disambiguates (color-blind safe).
let symbolName: String
/// Chinese status word shown as an optional label and always as the
/// accessibility label.
let label: String
/// The frozen mapping. Each status (color, distinct symbol, label).
static func style(for status: DisplayStatus) -> StatusStyle {
switch status {
case .working:
// solid filled circle actively running
return StatusStyle(color: DS.Palette.statusWorking, symbolName: "circle.fill", label: "运行中")
case .waiting:
// clock waiting on something
return StatusStyle(color: DS.Palette.statusWaiting, symbolName: "clock.fill", label: "等待中")
case .idle:
// hollow circle quiet/empty (gray, distinct from working's fill)
return StatusStyle(color: DS.Palette.statusIdle, symbolName: "circle", label: "空闲")
case .stuck:
// triangle alarm
return StatusStyle(color: DS.Palette.statusStuck, symbolName: "exclamationmark.triangle.fill", label: "卡住")
case .unknown:
// question mark no signal yet
return StatusStyle(color: DS.Palette.statusUnknown, symbolName: "questionmark.circle", label: "未知")
case .pendingApproval:
// filled ! circle "needs me", the single most important prompt
return StatusStyle(color: DS.Palette.statusWaiting, symbolName: "exclamationmark.circle.fill", label: "等待审批")
case .exited:
// checkered flag session over (mirrors ReconnectBanner's exited icon)
return StatusStyle(color: DS.Palette.statusExited, symbolName: "flag.checkered", label: "已退出")
}
}
/// Convenience bridge from the wire enum.
static func style(for claude: ClaudeStatus) -> StatusStyle {
style(for: DisplayStatus(claude))
}
}