import SwiftUI /// 顶层去处。设计定稿只有两处落点:首页(主机管理)与终端。 /// /// 关键:路由与**会话生命周期解耦** —— 回首页/最小化只改 route,会话照跑 /// (会话 surface 由 `SessionCanvas` 常驻挂载,见 ContentView)。 enum AppRoute: Equatable { case home case terminal(UUID) var sessionID: UUID? { if case .terminal(let id) = self { return id } return nil } } /// 侧边栏两态(用户裁决砍掉悬浮态:展开即固定)。同一条栏,元素不增不减,只是展示密度不同。 enum SidebarState: Equatable { /// 56pt 轨道:只显图标与两字缩写 case collapsed /// 288pt 固定在左侧:终端变窄。开合各触发一次 resize。 case pinned var isExpanded: Bool { self == .pinned } /// 该状态下侧边栏占据的布局宽度。 var layoutWidth: CGFloat { switch self { case .collapsed: TX.Layout.railWidth case .pinned: TX.Layout.sidebarWidth } } } /// 终端页上的模态浮层。放在 router 而不是视图 `@State`:⌘W 快捷键与无头 URL 驱动 /// 都要能打开它,视图内部状态触达不到。 enum AppModal: Equatable { /// 关闭确认(3c) case closeConfirm } /// 导航状态持有者。视图只读它、只经方法改它。 @MainActor final class AppRouter: ObservableObject { @Published var route: AppRoute = .home @Published var sidebar: SidebarState = .collapsed /// 当前模态浮层(关闭确认)。 @Published var modal: AppModal? /// 状态切换动效统一走 `TX.Motion.standard`(设计定稿 200–260ms easeOut,响应「减弱动态」)。 /// /// **路由 / 侧边栏 / 浮层的每一次变更都必须包在这条曲线里**,否则视图上的 `transition` /// 拿不到动画事务就退化成硬切 —— 首页 ⇄ 终端、关闭确认曾都是这么"啪"地跳出来的。 /// 收在 router 里的另一个好处:点击、⌘ 快捷键、无头 URL 三条入口天然同一手感。 private func animated(_ change: () -> Void) { withAnimation(TX.Motion.standard, change) } /// 进入某会话(点会话卡 / 侧边栏会话行 / 新建连接后)。恢复焦点由会话自身持有。 func enter(_ sessionID: UUID) { animated { route = .terminal(sessionID) } } /// 回首页:会话完全不动(⌘⇧H / 侧边栏首页行)。展开态跨导航保留。 func goHome() { animated { modal = nil route = .home } } /// 开合侧边栏(⌘\ / Logo 位展开键 / 头部收起键)。收起 ⇄ 固定展开。 func toggleSidebar() { animated { sidebar = sidebar == .collapsed ? .pinned : .collapsed } } /// 打开/关闭模态浮层(红点、⌘W 与无头 URL 共用同一入口)。 func present(_ modal: AppModal) { animated { self.modal = modal } } func dismissModal() { animated { modal = nil } } }