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:
@@ -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`),于是必须自己让位 —— 底栏在
|
||||
|
||||
Reference in New Issue
Block a user