feat: 业务逻辑对齐 + 首页/弹框七项减法 + 终端页四项体验修复

业务逻辑(用户定稿):
- 点主机总是新建会话(会话:主机=多:1);activate 选中断线会话自动重连
- 「直接输入」凭据保存时自动落 Keychain(去重复用),主机只存引用
- 主机编辑页可就地新建 Tailscale 连接;删无人引用的旧 HostsListView

首页/弹框减法:
- 删侧边栏「连接新主机」、无 tmux 横幅与安装引导、页头「新建连接」
- 搜索移到「全部主机」行;添加主机虚线卡与主机卡等高(骨架复刻)
- 会话卡加 ✕ + 复用终端页同款二次确认
- 会话选择器 sheet → 项目风格居中弹框(去手柄/Header/最近)

终端页修复:
- 侧边栏会话列表改创建顺序,切换不再重排抖动
- 焦点高亮上移到 pane 容器边框(四边完整,去负 padding)
- 软键盘双重让位修复:画布忽略键盘 + TXKeyboardObserver 显式让位一次
- 自动聚焦:focusTick 脉冲强制翻转 FocusState + surface 就绪补聚焦;
  vendor 补 iPadOS 触摸板 indirectPointer 的 becomeFirstResponder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-26 17:49:58 +08:00
parent 8da725114f
commit 4b5292107b
14 changed files with 378 additions and 470 deletions

View File

@@ -1,4 +1,5 @@
import SwiftUI
import UIKit
// 稿 / / docs/design/UI-DESIGN-HANDOFF.md
@@ -35,6 +36,41 @@ extension ButtonStyle where Self == TXPressStyle {
static var txPressTight: TXPressStyle { TXPressStyle(scale: 0.9, dim: 0.7) }
}
/// ****
///
/// keyboard avoidance `TX.Layout.footBand(safeBottom)`
/// **** footBand
/// 6 2026-07-26 `.ignoresSafeArea(.keyboard)`
/// `TerminalScreen` padding
/// frame 0
@MainActor
final class TXKeyboardObserver: ObservableObject {
@Published private(set) var height: CGFloat = 0
private var tokens: [NSObjectProtocol] = []
init() {
let nc = NotificationCenter.default
for name in [UIResponder.keyboardWillChangeFrameNotification,
UIResponder.keyboardWillHideNotification] {
tokens.append(nc.addObserver(forName: name, object: nil, queue: .main) { note in
let hide = note.name == UIResponder.keyboardWillHideNotification
let end = (note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?
.cgRectValue
Task { @MainActor [weak self] in self?.update(endFrame: end, hidden: hide) }
})
}
}
private func update(endFrame: CGRect?, hidden: Bool) {
guard let endFrame else { return }
let bounds = UIScreen.main.bounds
var overlap = hidden ? 0 : max(0, bounds.maxY - endFrame.minY)
// /
if endFrame.width < bounds.width - 1 { overlap = 0 }
if height != overlap { height = overlap }
}
}
/// +
///
/// 1. ****`ignoresSafeArea`