import SwiftUI // 设计稿 `3c 关闭确认`。(`3b 新建分屏菜单` 已随「放弃移动端分屏」移除。) // // 语义分工(务必别混): // - **黄点 / ⌘M / 下滑 = 最小化**:不确认,直接回首页,会话继续跑。 // - **红点 / ⌘W = 关闭**:弹一次确认,按主机能力两套文案;`esc` 取消、`⏎` 选推荐项。 /// 浮层容器:遮罩 + 居中卡片(对话框 / 菜单共用)。 /// /// 遮罩淡入、卡片从 96% 长出来(`TX.Motion.pop`)—— 两段分开写才有层次;整块套一个 /// `.transition(.opacity)` 的话卡片只是跟着变淡,弹出感全无。 /// 动画事务由 `AppRouter.present/dismissModal` 提供。 struct TXModalScrim: View { let onDismiss: () -> Void @ViewBuilder let content: Content var body: some View { ZStack { TX.Scrim.color.opacity(TX.Scrim.dialog) .ignoresSafeArea() .contentShape(Rectangle()) .onTapGesture(perform: onDismiss) .transition(.opacity) content .transition(TX.Motion.pop) } } } // MARK: - 关闭确认(3c) /// 关闭确认。两套文案由会话能力决定: /// - **tmux 会话**:推荐「仅断开」(`detach-client`,服务端继续跑);左侧次要文字按钮「结束会话」 /// (`kill-session`,**需再确认一次**)。 /// - **原生窗口**:琥珀警告「没有 tmux 兜底,前台进程会随之结束」,推荐「取消」,危险按钮「关闭窗口」。 struct CloseConfirmDialog: View { @ObservedObject var session: TerminalSession /// 仅断开(tmux)/ 直接关闭(原生窗口) let onDetach: () -> Void /// 结束 tmux 会话(kill-session) let onKill: () -> Void let onCancel: () -> Void @EnvironmentObject var theme: TXThemeManager @State private var confirmingKill = false private var p: TXPalette { theme.palette } private var isTmux: Bool { session.tmuxSummary != nil } var body: some View { TXModalScrim(onDismiss: onCancel) { VStack(alignment: .leading, spacing: 0) { Text(title) .font(TX.Font.mono(15, .medium)) .foregroundStyle(p.text) .padding(.bottom, 8) message .padding(.bottom, TX.Space.l) buttons } .padding(TX.Space.l) .frame(width: isTmux ? 436 : 404) .background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.dialog)) .overlay( RoundedRectangle(cornerRadius: TX.Radius.dialog) .strokeBorder(p.borderStrong, lineWidth: 1) ) .txShadow(TX.Elevation.dialog) } } private var title: String { if let t = session.tmuxSummary, !t.name.isEmpty { return "关闭 \(session.displayName) · \(t.name)?" } // 原生窗口带窗口编号(首版每主机单窗口,恒为 1;多窗口是二期能力)。 return "关闭 \(session.displayName) · 窗口 1?" } @ViewBuilder private var message: some View { if let t = session.tmuxSummary { Text("tmux 会话 (\(t.windows) windows / \(t.panes) panes)。仅断开后服务端继续跑,下次原样接回。") .font(TX.Font.mono(12)) .foregroundStyle(p.text3) .lineSpacing(4) } else { Text("没有 tmux 兜底,") .font(TX.Font.mono(12)) .foregroundStyle(p.text3) + Text("前台进程会随之结束") .font(TX.Font.mono(12, .medium)) .foregroundStyle(p.warning) + Text("。想留着就点黄点最小化。") .font(TX.Font.mono(12)) .foregroundStyle(p.text3) } } @ViewBuilder private var buttons: some View { HStack(spacing: TX.Space.s) { if isTmux { // 次要文字按钮(危险):kill-session 需再点一次确认。 Button { if confirmingKill { onKill() } else { confirmingKill = true } } label: { Text(confirmingKill ? "确认结束会话?" : "结束会话") .font(TX.Font.mono(12, confirmingKill ? .semibold : .regular)) .foregroundStyle(p.danger) } .buttonStyle(.txPress) } Spacer() TXButton(title: "取消", action: onCancel) if isTmux { TXButton(title: "仅断开 ⏎", emphasized: true, action: onDetach) } else { dangerButton } } } /// 危险按钮(设计给的精确色:描边 #8d5560 / 底 rgba(92,58,63,.35) / 字 #f5a0ac)。 private var dangerButton: some View { Button(action: onDetach) { Text("关闭窗口") .font(TX.Font.mono(12, .medium)) .foregroundStyle(Color(hex: 0xF5A0AC)) .padding(.horizontal, 14) .frame(height: 32) .background(Color(hex: 0x5C3A3F).opacity(0.35), in: RoundedRectangle(cornerRadius: TX.Radius.control)) .overlay( RoundedRectangle(cornerRadius: TX.Radius.control) .strokeBorder(Color(hex: 0x8D5560), lineWidth: 1) ) } .buttonStyle(.txPress) } }