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:
86
ios/App/WebTerm/DesignSystem/StatusStyle.swift
Normal file
86
ios/App/WebTerm/DesignSystem/StatusStyle.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user