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:
@@ -24,12 +24,6 @@ struct QuickReplyBar: View {
|
||||
static let managePhrases = "管理常用语"
|
||||
}
|
||||
|
||||
private enum Metrics {
|
||||
static let chipSpacing: CGFloat = 6
|
||||
static let rowHorizontalPadding: CGFloat = 8
|
||||
static let rowVerticalPadding: CGFloat = 6
|
||||
}
|
||||
|
||||
let terminalViewModel: TerminalViewModel
|
||||
let gateViewModel: GateViewModel
|
||||
let store: QuickReplyStore
|
||||
@@ -62,16 +56,19 @@ struct QuickReplyBar: View {
|
||||
|
||||
private var chipRow: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Metrics.chipSpacing) {
|
||||
HStack(spacing: DS.Space.sm8) {
|
||||
ForEach(store.allChips) { chip in
|
||||
chipButton(chip)
|
||||
}
|
||||
managePhrasesButton
|
||||
}
|
||||
.padding(.horizontal, Metrics.rowHorizontalPadding)
|
||||
.padding(.vertical, Metrics.rowVerticalPadding)
|
||||
.padding(.horizontal, DS.Space.sm8)
|
||||
.padding(.vertical, DS.Space.xs4)
|
||||
}
|
||||
.background(.thinMaterial, in: Capsule())
|
||||
.background(.regularMaterial, in: Capsule())
|
||||
.overlay(
|
||||
Capsule().strokeBorder(DS.Palette.hairline, lineWidth: DS.Stroke.hairline)
|
||||
)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
|
||||
@@ -82,11 +79,14 @@ struct QuickReplyBar: View {
|
||||
// Text(verbatim:) — user/label strings render as inert text, never
|
||||
// LocalizedStringKey/Markdown (SEC-L3 mirror; T-iOS-23 convention).
|
||||
Text(verbatim: chip.label)
|
||||
.font(DS.Typography.callout)
|
||||
.foregroundStyle(DS.Palette.accent)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, DS.Space.md12)
|
||||
.frame(minHeight: DS.Layout.minHitTarget)
|
||||
.background(.quaternary, in: Capsule())
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.small)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var managePhrasesButton: some View {
|
||||
@@ -94,10 +94,12 @@ struct QuickReplyBar: View {
|
||||
isPanelPresented = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(DS.Typography.callout.weight(.semibold))
|
||||
.foregroundStyle(DS.Palette.accent)
|
||||
.frame(width: DS.Layout.minHitTarget, height: DS.Layout.minHitTarget)
|
||||
.background(.quaternary, in: Capsule())
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.small)
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Copy.managePhrases)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +157,8 @@ struct QuickReplyPanel: View {
|
||||
Section(Copy.customSection) {
|
||||
if store.userChips.isEmpty {
|
||||
Text(Copy.emptyState)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(DS.Typography.callout)
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
} else {
|
||||
ForEach(store.userChips) { chip in
|
||||
Button {
|
||||
@@ -163,7 +166,7 @@ struct QuickReplyPanel: View {
|
||||
} label: {
|
||||
chipRow(chip)
|
||||
}
|
||||
.tint(.primary)
|
||||
.tint(DS.Palette.textPrimary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
// Resolve ids FIRST — removing while iterating offsets
|
||||
@@ -181,15 +184,16 @@ struct QuickReplyPanel: View {
|
||||
}
|
||||
|
||||
private func chipRow(_ chip: QuickReplyChip) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: DS.Space.xs2) {
|
||||
// Text(verbatim:) — stored strings are boundary data (SEC-L3 mirror).
|
||||
Text(verbatim: chip.label)
|
||||
.font(DS.Typography.body)
|
||||
.lineLimit(1)
|
||||
Text(verbatim: chip.appendEnter
|
||||
? chip.text + Copy.enterSuffixSymbol
|
||||
: chip.text)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(DS.Typography.caption)
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user