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

@@ -17,13 +17,13 @@ struct DiffScreen: View {
@State private var viewModel: DiffViewModel
private enum Metrics {
static let pickerHorizontalPadding: CGFloat = 16
static let pickerVerticalPadding: CGFloat = 8
static let lineVerticalInset: CGFloat = 1
static let lineHorizontalInset: CGFloat = 12
static let fileHeaderTopInset: CGFloat = 14
static let statTagSpacing: CGFloat = 8
static let lineBackgroundOpacity = 0.12
/// Faint per-line tint alpha for added/removed/hunk rows. This is a
/// **code-diff syntax constant** (like terminal/xterm colors), NOT a
/// card/status design token the frozen `DS.Opacity` scale
/// (stale/exited/pressed) has no semantic slot for a syntax highlight
/// fill. The tint HUE always comes from `DS.Palette` (below); only this
/// scanning-aid alpha is diff-local.
static let lineTintOpacity = 0.12
}
/// T-iOS-26 `(endpoint, path)` +
@@ -41,8 +41,8 @@ struct DiffScreen: View {
var body: some View {
VStack(spacing: 0) {
scopePicker
.padding(.horizontal, Metrics.pickerHorizontalPadding)
.padding(.vertical, Metrics.pickerVerticalPadding)
.padding(.horizontal, DS.Space.lg16)
.padding(.vertical, DS.Space.sm8)
content
}
.navigationTitle(DiffCopy.title)
@@ -104,6 +104,7 @@ struct DiffScreen: View {
Task { await viewModel.load() }
}
.buttonStyle(.borderedProminent)
.tint(DS.Palette.accent)
}
}
@@ -136,8 +137,8 @@ struct DiffScreen: View {
private var truncatedBanner: some View {
Label(DiffCopy.truncatedBanner, systemImage: "scissors")
.font(.footnote)
.foregroundStyle(.orange)
.font(DS.Typography.caption)
.foregroundStyle(DS.Palette.statusWaiting)
}
@ViewBuilder private func rowView(_ row: DiffRow) -> some View {
@@ -146,13 +147,13 @@ struct DiffScreen: View {
fileHeaderRow(header)
case .binaryNotice:
Text(DiffCopy.binaryFile)
.font(.caption.italic())
.foregroundStyle(.secondary)
.font(DS.Typography.caption.italic())
.foregroundStyle(DS.Palette.textSecondary)
.listRowSeparator(.hidden)
case .hunkHeader(let header):
diffTextRow(
header, color: .blue,
background: Color.blue.opacity(Metrics.lineBackgroundOpacity)
header, color: DS.Palette.accent,
background: DS.Palette.accent.opacity(Metrics.lineTintOpacity)
)
case .line(let kind, let text):
diffTextRow(
@@ -162,24 +163,25 @@ struct DiffScreen: View {
}
private func fileHeaderRow(_ header: DiffFileHeader) -> some View {
HStack(spacing: Metrics.statTagSpacing) {
// verbatim +
HStack(spacing: DS.Space.sm8) {
// verbatim +
Text(verbatim: header.pathLabel)
.font(.footnote.weight(.semibold).monospaced())
.font(DS.Typography.mono(.footnote).weight(.semibold))
.foregroundStyle(DS.Palette.textPrimary)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 0)
Text(verbatim: "+\(header.added)")
.font(.caption.monospacedDigit())
.foregroundStyle(.green)
.font(DS.Typography.mono(.caption))
.foregroundStyle(DS.Palette.statusWorking)
Text(verbatim: "-\(header.removed)")
.font(.caption.monospacedDigit())
.foregroundStyle(.red)
.font(DS.Typography.mono(.caption))
.foregroundStyle(DS.Palette.statusStuck)
Text(DiffStatusStyle.label(for: header.status))
.font(.caption2)
.font(DS.Typography.caption)
.foregroundStyle(DiffStatusStyle.color(for: header.status))
}
.padding(.top, Metrics.fileHeaderTopInset)
.padding(.top, DS.Space.lg16)
.accessibilityElement(children: .combine)
}
@@ -187,7 +189,7 @@ struct DiffScreen: View {
/// single-line tail truncation (read-only skim view; no wrapping blob).
private func diffTextRow(_ text: String, color: Color, background: Color) -> some View {
Text(verbatim: text)
.font(.caption.monospaced())
.font(DS.Typography.mono(.caption))
.foregroundStyle(color)
.lineLimit(1)
.truncationMode(.tail)
@@ -195,30 +197,32 @@ struct DiffScreen: View {
.listRowBackground(background)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(
top: Metrics.lineVerticalInset,
leading: Metrics.lineHorizontalInset,
bottom: Metrics.lineVerticalInset,
trailing: Metrics.lineHorizontalInset
top: DS.Space.xs2,
leading: DS.Space.md12,
bottom: DS.Space.xs2,
trailing: DS.Space.md12
))
}
// MARK: - kind kind .context
// DS.Paletteadded=working 绿 / removed=stuck /
// hunk=accent App alpha diff
static func lineColor(_ kind: DiffLineKind) -> Color {
switch kind {
case .added: return .green
case .removed: return .red
case .context: return .primary
case .hunk: return .blue
case .meta: return .secondary
case .added: return DS.Palette.statusWorking
case .removed: return DS.Palette.statusStuck
case .context: return DS.Palette.textPrimary
case .hunk: return DS.Palette.accent
case .meta: return DS.Palette.textSecondary
}
}
static func lineBackground(_ kind: DiffLineKind) -> Color {
switch kind {
case .added: return .green.opacity(Metrics.lineBackgroundOpacity)
case .removed: return .red.opacity(Metrics.lineBackgroundOpacity)
case .hunk: return .blue.opacity(Metrics.lineBackgroundOpacity)
case .added: return DS.Palette.statusWorking.opacity(Metrics.lineTintOpacity)
case .removed: return DS.Palette.statusStuck.opacity(Metrics.lineTintOpacity)
case .hunk: return DS.Palette.accent.opacity(Metrics.lineTintOpacity)
case .context, .meta: return .clear
}
}
@@ -240,10 +244,10 @@ enum DiffStatusStyle {
static func color(for status: DiffFileStatus) -> Color {
switch status {
case .added, .untracked: return .green
case .deleted: return .red
case .renamed: return .blue
case .modified, .binary: return .secondary
case .added, .untracked: return DS.Palette.statusWorking
case .deleted: return DS.Palette.statusStuck
case .renamed: return DS.Palette.accent
case .modified, .binary: return DS.Palette.textSecondary
}
}
}