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:
@@ -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.Palette(added=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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user