feat: 侧边栏两态重构(统一视图身份)+ 第四轮触控化走查

侧边栏(真机验证):
- 砍掉悬浮态,只留收起⇄固定展开;TerminalSidebar 单一视图身份,
  内容恒按 288pt 排版、收起=左侧裁切(clipped 宽度动画),
  图标列两态同尺寸同位置,明细淡入淡出,终端随宽度盖上/让开
- 开合入口收敛:Logo 位(收起态=»展开键/展开态=Logo)+ 头部«;
  删底部开合键与 Pin;LeadingHitShape 修 clipped 不裁触摸
- 修 GridBox 从不通知 UI:onGridChange→objectWillChange,
  标题胶囊格子数随开合实时刷新

触控化走查(上一轮,真机部署验证):
- 窗口按钮 13pt 色点→22pt 圆角方块常显字形;动作条改可点按钮
- TXTech 拼写/配色规范(SSH/mosh/tmux 官方写法+品牌色映射)
- 首页会话卡固定 3 列 4:3;去掉未实现的 ⌘K 提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-26 00:55:53 +08:00
parent 28e9cfc207
commit 5707d70dff
15 changed files with 1086 additions and 675 deletions

View File

@@ -94,7 +94,8 @@ struct RootView: View {
if router.route == .home {
HomeView(manager: manager, router: router, hostStore: hostStore,
credStore: credStore, tailscaleStore: tailscaleStore)
.transition(.opacity)
// 97% ""
.transition(TX.Motion.pushBack)
}
}
.background(p.bgDeep.ignoresSafeArea())
@@ -142,6 +143,7 @@ struct SessionCanvas: View {
@ObservedObject var hostStore: HostStore
@ObservedObject var credStore: FileCredentialStore
@ObservedObject var tailscaleStore: TailscaleStore
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
ZStack {
@@ -153,61 +155,55 @@ struct SessionCanvas: View {
// ""
// `scaleEffect` frame ** PTY resize**
// frame tmux
.scaleEffect(visible ? 1 : 0.97)
.animation(AppRouter.sidebarAnimation, value: visible)
.scaleEffect(visible || reduceMotion ? 1 : 0.97)
.animation(TX.Motion.standard, value: visible)
.allowsHitTesting(visible)
}
}
}
}
/// · 稿 `1b` + `3a`
/// · 稿 `1b` + `3a`/
///
/// 56pt Pin 288pt + 86pt
/// 34pt **** reflow resize
/// Pin resize
/// `TerminalSidebar`56 288+ 86pt
///
/// resize
///
/// **** chrome ****`ignoresSafeArea()`
/// / `TX.Layout.footBand`线 y
struct TerminalScreen: View {
@ObservedObject var session: TerminalSession
@ObservedObject var manager: SessionManager
@ObservedObject var router: AppRouter
@ObservedObject var tailscaleStore: TailscaleStore
@EnvironmentObject var theme: TXThemeManager
@Environment(\.horizontalSizeClass) private var hSize
@State private var sidebarSearch = ""
/// tmux
@State private var showTmuxInstall = false
private var p: TXPalette { theme.palette }
var body: some View {
HStack(spacing: 0) {
if router.sidebar == .pinned {
sidebarPanel
} else {
RailView(manager: manager, router: router, session: session,
onNewHost: goHomeToAddHost, onSettings: goHomeToAddHost)
}
stage
}
.background(p.bgDeep.ignoresSafeArea())
// 稿 1b/3a /logo RootView
// SessionCanvas hide
.ignoresSafeArea(edges: .top)
// 50%
// + 200260ms easeOut AppRouter
.overlay(alignment: .leading) {
if router.sidebar == .overlay {
ZStack(alignment: .leading) {
Color(hex: 0x0A0B12).opacity(0.5)
.ignoresSafeArea()
.contentShape(Rectangle())
.onTapGesture { router.dismissOverlaySidebar() }
.transition(.opacity)
sidebarPanel
.transition(.move(edge: .leading))
}
// safe area
// TXScreenMetrics
TXScreenMetrics { geo in
screen(safeTop: geo.safeAreaInsets.top, safeBottom: geo.safeAreaInsets.bottom)
.ignoresSafeArea()
}
}
}
private func screen(safeTop: CGFloat, safeBottom: CGFloat) -> some View {
HStack(spacing: 0) {
// **** TerminalSidebar
// 56 288
// resize
TerminalSidebar(manager: manager, router: router, session: session,
tailscaleStore: tailscaleStore,
railWidth: railWidth, safeBottom: safeBottom,
onNewHost: goHomeToAddHost, onSettings: goHomeToAddHost,
search: $sidebarSearch)
stage(safeTop: safeTop, safeBottom: safeBottom)
}
.background(p.bgDeep)
// 3c3b alert/sheet
.overlay {
switch router.modal {
@@ -230,10 +226,10 @@ struct TerminalScreen: View {
if showTmuxInstall {
TmuxInstallSheet(
onRun: { cmd in
showTmuxInstall = false
withAnimation(TX.Motion.standard) { showTmuxInstall = false }
session.runInTerminal(cmd)
},
onCancel: { showTmuxInstall = false })
onCancel: { withAnimation(TX.Motion.standard) { showTmuxInstall = false } })
}
}
// host key 使
@@ -248,11 +244,9 @@ struct TerminalScreen: View {
}
}
private var sidebarPanel: SidebarPanel {
SidebarPanel(manager: manager, router: router, session: session,
tailscaleStore: tailscaleStore,
onNewHost: goHomeToAddHost, onSettings: goHomeToAddHost,
search: $sidebarSearch)
/// / 48
private var railWidth: CGFloat {
hSize == .compact ? TX.Layout.railWidthCompact : TX.Layout.railWidth
}
/// + / 齿
@@ -263,13 +257,15 @@ struct TerminalScreen: View {
// MARK: -
private var stage: some View {
private func stage(safeTop: CGFloat, safeBottom: CGFloat) -> some View {
ZStack(alignment: .top) {
VStack(spacing: 0) {
// tmux 2c pane
if session.tmuxUnavailable {
NoTmuxBanner(onInstall: { showTmuxInstall = true },
onDismiss: { session.dismissTmuxBanner() })
NoTmuxBanner(onInstall: { withAnimation(TX.Motion.standard) { showTmuxInstall = true } },
onDismiss: {
withAnimation(TX.Motion.standard) { session.dismissTmuxBanner() }
})
.padding(.top, TX.Layout.capsuleInset)
.transition(.opacity)
}
@@ -280,20 +276,35 @@ struct TerminalScreen: View {
.padding(.top, session.tmuxUnavailable ? TX.Space.hairline : TX.Layout.capsuleInset)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(p.terminalBackground)
.clipShape(RoundedRectangle(cornerRadius: 6))
.clipShape(paneShape)
.overlay(
RoundedRectangle(cornerRadius: 6).strokeBorder(p.border, lineWidth: 1)
paneShape.strokeBorder(p.border, lineWidth: 1)
// /
// 线
.padding(EdgeInsets(top: -1, leading: 0, bottom: 0, trailing: -1))
)
// pane 线
.padding([.leading, .trailing, .top], TX.Layout.paneGap)
TerminalHintBar(context: hintContext, hints: hints)
// **** 2pt /
// pane 线
.padding(.leading, TX.Layout.paneGap)
TerminalActionBar(context: barContext, actions: barActions, safeBottom: safeBottom)
}
capsules
capsules(safeTop: safeTop)
}
.background(p.bgDeep)
.contentShape(Rectangle())
// Pin
.simultaneousGesture(TapGesture().onEnded { router.dismissOverlaySidebar() })
}
/// pane ** app chrome **
///
/// iPad/iPhone 6pt
/// ""
/// /****
/// pane chrome Stage Manager
private var paneShape: UnevenRoundedRectangle {
UnevenRoundedRectangle(
topLeadingRadius: TX.Radius.pane,
bottomLeadingRadius: TX.Radius.pane,
bottomTrailingRadius: TX.Radius.pane,
topTrailingRadius: 0)
}
@ViewBuilder private var paneArea: some View {
@@ -312,17 +323,20 @@ struct TerminalScreen: View {
router.goHome()
manager.close(id)
})
//
.animation(TX.Motion.standard, value: session.phaseText)
}
}
private var capsules: some View {
private func capsules(safeTop: CGFloat) -> some View {
HStack(alignment: .top) {
TerminalTitleCapsule(session: session, onClose: requestClose, onMinimize: minimizeSession)
Spacer(minLength: TX.Space.m)
TerminalStatusCapsule(session: session)
}
.padding(.horizontal, TX.Space.m)
.padding(.top, 14)
// iPad 14/
.padding(.top, TX.Layout.capsuleTop(safeTop))
}
/// **** `closeSession(kill:)`
@@ -337,7 +351,7 @@ struct TerminalScreen: View {
if let tmux = session.tmuxController {
if kill { tmux.killSession() } else { tmux.detach() }
}
withAnimation(AppRouter.sidebarAnimation) { router.goHome() }
router.goHome() // router TX.Motion.standard
// detach/kill tmux %exit onExit
Task { @MainActor in
try? await Task.sleep(nanoseconds: 250_000_000)
@@ -348,7 +362,7 @@ struct TerminalScreen: View {
private func minimizeSession() {
session.captureSnapshot() //
manager.minimize(session.id)
withAnimation(AppRouter.sidebarAnimation) { router.goHome() }
router.goHome()
}
///
@@ -373,20 +387,31 @@ struct TerminalScreen: View {
router.present(.splitMenu(vertical: vertical))
}
private var hintContext: String {
switch router.sidebar {
case .pinned: "Pin 态 · 终端变窄,仅在钉住/解除时 resize 一次"
case .overlay: "遮罩态 · 点终端任意处收起,终端不 reflow"
case .collapsed: session.isLive ? "已连接妙控键盘 · 附加行已收起" : session.phaseText
/// **** ·
///
private var barContext: String {
if !session.isLive { return session.phaseText }
if let t = session.tmuxSummary {
let name = t.name.isEmpty ? "tmux" : "tmux \(t.name)"
return "\(session.displayName) · \(name) · \(t.windows) window\(t.windows == 1 ? "" : "s")"
}
if session.tmuxSkipped { return "\(session.displayName) · 原生窗口" }
return session.displayName
}
private var hints: [(key: String, label: String)] {
switch router.sidebar {
case .pinned: [("Pin", "解除钉住"), ("\\", "收起为轨道"), ("⌘⇧H", "回首页")]
case .overlay: [("\\", "收起"), ("Pin", "钉在左侧"), ("⌘⇧H", "回首页")]
case .collapsed: [("⌘K", "命令面板"), ("⌘D", "竖分屏"), ("⌘T", "新 window"), ("⌘⇧H", "回首页")]
}
/// ****tmux
private var barActions: [TerminalActionBar.Action] {
let tmux = session.tmuxController
return [
.init(id: "split", icon: "square.split.2x1", title: "竖分屏", enabled: tmux != nil) {
requestSplit(vertical: true)
},
.init(id: "window", icon: "plus.rectangle.on.rectangle", title: "新建 window",
enabled: tmux != nil) {
tmux?.newWindow()
},
.init(id: "home", icon: "house", title: "回首页") { router.goHome() },
]
}
}