Files
web-terminal/ios/App/WebTerm/Wiring/PrivacyShade.swift
Yaojia Wang 660a40491a 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/.
2026-07-05 22:00:31 +02:00

71 lines
3.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
/// T-iOS-15 · Privacy shade (security-critical, plan §7 T-iOS-15).
///
/// iOS writes the app-switcher snapshot to DISK the moment the scene reaches
/// `.background` but the switcher is usually ENTERED at `.inactive`. The
/// shade therefore covers whenever `scenePhase != .active` (the exact rule;
/// `.inactive`-only would let the `.background` snapshot capture terminal
/// content API keys, tokens, source into the on-disk switcher image).
/// Restored on `.active`.
///
/// Pure mapping split from the view so the rule is unit-testable for all
/// three phases (PrivacyShadeTests).
enum PrivacyShadePolicy {
static func isShadeVisible(for scenePhase: ScenePhase) -> Bool {
scenePhase != .active
}
}
/// Opaque cover rendered ABOVE the whole navigation tree (RootView ZStack)
/// deliberately no animation: the snapshot moment must never race a fade.
///
/// Security invariant (do NOT weaken): the FULL-SCREEN base is `DS.Palette.surface`
/// (`.systemBackground`, fully OPAQUE) + `.ignoresSafeArea()`, so no terminal
/// byte can leak into the on-disk switcher snapshot. The branded lockup is a
/// `.regularMaterial` card layered ON TOP of that opaque base (never over the
/// terminal), so the material's translucency is purely decorative it blurs
/// the opaque surface below it, not the PTY content. Accent lock + app name +
/// hint give the shade a refined-native identity instead of a flat cover.
///
/// Scope note (documented): sheets present in a separate presentation layer a
/// root overlay cannot cover but no terminal bytes are ever rendered in a
/// sheet (pairing / plan-gate only), so the root-level shade covers every
/// surface that shows PTY content.
struct PrivacyShadeView: View {
var body: some View {
ZStack {
// OPAQUE full-screen occlusion the security base. Must stay opaque
// and edge-to-edge; the material card below only decorates it.
DS.Palette.surface
.ignoresSafeArea()
VStack(spacing: DS.Space.md12) {
Image(systemName: "lock.fill")
.font(DS.Typography.largeTitle)
.foregroundStyle(DS.Palette.accent)
Text(ShadeCopy.appName)
.font(DS.Typography.headline)
.foregroundStyle(DS.Palette.textPrimary)
Text(ShadeCopy.hint)
.font(DS.Typography.caption)
.foregroundStyle(DS.Palette.textSecondary)
}
.padding(.horizontal, DS.Space.xxl24)
.padding(.vertical, DS.Space.xl20)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: DS.Radius.lg16))
.overlay(
RoundedRectangle(cornerRadius: DS.Radius.lg16)
.strokeBorder(DS.Palette.hairline, lineWidth: DS.Stroke.hairline)
)
}
.accessibilityHidden(true)
}
}
/// Chinese, named constants
private enum ShadeCopy {
static let appName = "WebTerm"
static let hint = "内容已隐藏"
}