feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
This commit is contained in:
98
ios/App/WebTerm/DesignSystem/TerminalPalette.swift
Normal file
98
ios/App/WebTerm/DesignSystem/TerminalPalette.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// T-iOS-34 · 终端画布配色 —— **跟随主题**的那一半。
|
||||
///
|
||||
/// 此前终端是"固定暖深色,不随外观变"(对齐桌面)。浅色主题落地后这条不成立:
|
||||
/// 用户选了浅色,却有一整块纯黑画布占满屏,是最刺眼的破绽。所以画布按有效外观
|
||||
/// 取两套值:
|
||||
/// - 深色 = `#100F0D` / `#ECE9E3`(桌面 web `--bg` / `--text`,**逐字节不变**);
|
||||
/// - 浅色 = `#F6F7F9` / `#1A1D24`(镜像 web `public/settings.ts` 的
|
||||
/// `THEMES.light`,iOS 与桌面浅色档观感一致)。
|
||||
///
|
||||
/// caret / selection 用**该档**的 accent 解析值(深色金 `#E3A64A` / 浅色深金
|
||||
/// `#C9892F`),不写死深色档的金。
|
||||
///
|
||||
/// 说明(重要):`SwiftTerm.TerminalView` 在 `nativeBackgroundColor` 的 setter 里
|
||||
/// 就把 UIColor 压成了内部 `terminal.backgroundColor`(`iOSTerminalView.swift:1207`),
|
||||
/// 且没有 `traitCollectionDidChange` 重取 —— 因此**动态 UIColor 只能保证"构建时
|
||||
/// 取对档"**,主题在会话进行中切换必须由视图侧重新 `apply` 一次。本文件提供两种
|
||||
/// 形态供调用点选择:`colors(for:)`(已解析,重套用)与 `dynamic*`(动态 UIColor,
|
||||
/// 构建时取当前 trait)。
|
||||
enum TerminalPalette {
|
||||
|
||||
/// 一档终端画布的四个颜色。全部是**已解析**的具体色(不是动态色),
|
||||
/// 便于直接交给 SwiftTerm,也便于测试逐字节断言。
|
||||
struct Colors: Equatable {
|
||||
let background: UIColor
|
||||
let foreground: UIColor
|
||||
let caret: UIColor
|
||||
let selection: UIColor
|
||||
}
|
||||
|
||||
/// 深色档(桌面对齐,历史值)。
|
||||
private enum Dark {
|
||||
static let background: UInt32 = 0x100F_0D
|
||||
static let foreground: UInt32 = 0xECE9_E3
|
||||
}
|
||||
|
||||
/// 浅色档(镜像 web `THEMES.light`)。
|
||||
private enum Light {
|
||||
static let background: UInt32 = 0xF6F7_F9
|
||||
static let foreground: UInt32 = 0x1A1D_24
|
||||
}
|
||||
|
||||
static func colors(for scheme: ColorScheme) -> Colors {
|
||||
let style = scheme.userInterfaceStyle
|
||||
let accent = DS.Palette.accentUIColor()
|
||||
.resolvedColor(with: UITraitCollection(userInterfaceStyle: style))
|
||||
let isDark = style == .dark
|
||||
return Colors(
|
||||
background: UIColor(hex: isDark ? Dark.background : Light.background),
|
||||
foreground: UIColor(hex: isDark ? Dark.foreground : Light.foreground),
|
||||
caret: accent,
|
||||
selection: accent
|
||||
)
|
||||
}
|
||||
|
||||
/// 动态背景色 —— 取值时机决定档位(SwiftUI/UIKit 的 trait 解析)。
|
||||
static func dynamicBackground() -> UIColor {
|
||||
UIColor { trait in colors(for: trait.colorScheme).background }
|
||||
}
|
||||
|
||||
/// 动态前景色。
|
||||
static func dynamicForeground() -> UIColor {
|
||||
UIColor { trait in colors(for: trait.colorScheme).foreground }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 外观桥(ColorScheme ↔ UIUserInterfaceStyle)
|
||||
|
||||
extension ColorScheme {
|
||||
/// SwiftUI → UIKit。`ColorScheme` 只有 light/dark 两个 case,故是全函数。
|
||||
var userInterfaceStyle: UIUserInterfaceStyle {
|
||||
self == .dark ? .dark : .light
|
||||
}
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
/// UIKit → SwiftUI。`.unspecified` 按 iOS 的默认外观算作浅色。
|
||||
var colorScheme: ColorScheme {
|
||||
userInterfaceStyle == .dark ? .dark : .light
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hex
|
||||
|
||||
extension UIColor {
|
||||
/// 从 `0xRRGGBB` 构建不透明 sRGB 色。设计 token 的十六进制值在文档/CSS 里
|
||||
/// 就是这个写法,直接用它可以逐字节对照,不必手算 0–1 分量。
|
||||
convenience init(hex: UInt32, alpha: CGFloat = 1) {
|
||||
self.init(
|
||||
red: CGFloat((hex >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(hex & 0xFF) / 255,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user