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:
@@ -6,6 +6,10 @@ import SwiftUI
|
||||
// 终端内容容器顶部留 86pt 预留带给胶囊,预留带用 padding 实现(容器 clipped 防向上溢出)。
|
||||
|
||||
/// 浮起胶囊的共用外观:blur 材质 + 半透底 + 常规描边 + 圆角 11。
|
||||
///
|
||||
/// **扁平**:分层交给描边和材质,投影只留一点点(`TX.Elevation.floating`)。原先那道
|
||||
/// `0 10px 30px rgba(0,0,0,.55)` 在终端文字上糊出一大团脏斑 —— 胶囊底本身是半透的,
|
||||
/// 投影一重就成了"漏墨"。底色相应提到 .92,少了投影仍然读得清。
|
||||
private struct FloatingCapsule<Content: View>: View {
|
||||
@EnvironmentObject var theme: TXThemeManager
|
||||
@ViewBuilder let content: Content
|
||||
@@ -17,14 +21,14 @@ private struct FloatingCapsule<Content: View>: View {
|
||||
.frame(height: 34)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: TX.Radius.capsule)
|
||||
.fill(p.surfaceDark.opacity(0.88))
|
||||
.fill(p.surfaceDark.opacity(0.92))
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: TX.Radius.capsule))
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: TX.Radius.capsule)
|
||||
.strokeBorder(p.border, lineWidth: 1)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.55), radius: 15, y: 10)
|
||||
.txShadow(TX.Elevation.floating)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +43,17 @@ struct TerminalTitleCapsule: View {
|
||||
var body: some View {
|
||||
FloatingCapsule {
|
||||
HStack(spacing: TX.Space.xs) {
|
||||
windowDot(TXWindowDot.close, "关闭会话", action: onClose)
|
||||
windowDot(TXWindowDot.minimize, "最小化", action: onMinimize)
|
||||
// 顺序沿用 macOS 的肌肉记忆(关闭在左、最小化在右),但形态改成带字形的按钮,
|
||||
// 详见 windowButton。
|
||||
windowButton(icon: "xmark", tint: TXWindowDot.close,
|
||||
label: "关闭会话", action: onClose)
|
||||
// 向下的 chevron 而不是双向内收箭头:后者在 9.5pt 下糊成一团认不出。
|
||||
// 向下同时对上设计里的「下滑最小化」手势语义。
|
||||
windowButton(icon: "chevron.down", tint: TXWindowDot.minimize,
|
||||
label: "最小化到首页", action: onMinimize)
|
||||
Rectangle().fill(p.border).frame(width: 1, height: 14)
|
||||
.padding(.horizontal, 2)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 2)
|
||||
TXStatusDot(color: RailSidebarStyle.statusColor(session, p), size: 6)
|
||||
Text(session.displayName)
|
||||
.font(TX.Font.mono(12.5, .medium))
|
||||
@@ -54,7 +65,8 @@ struct TerminalTitleCapsule: View {
|
||||
.foregroundStyle(p.textWeak)
|
||||
.lineLimit(1)
|
||||
}
|
||||
// 终端格子数(设计稿 08 的「· 168×44」)。首次布局后稳定,故不必 @Published。
|
||||
// 终端格子数(设计稿 08 的「· 168×44」)。侧边栏开合会 resize ——
|
||||
// 数字经 GridBox.onGridChange → session.objectWillChange 跟着刷新。
|
||||
Text(gridLabel)
|
||||
.font(TX.Font.mono(11.5))
|
||||
.foregroundStyle(p.textFaint)
|
||||
@@ -91,16 +103,30 @@ struct TerminalTitleCapsule: View {
|
||||
return "\(g.cols)×\(g.rows)"
|
||||
}
|
||||
|
||||
private func windowDot(_ color: Color, _ label: String, action: @escaping () -> Void) -> some View {
|
||||
/// 关闭 / 最小化按钮。
|
||||
///
|
||||
/// 原来是 macOS 交通灯式的 13pt 纯色圆点,三处不合适:① 纯色块**不说明自己是干什么的** ——
|
||||
/// mac 上靠 hover 才浮出字形,触屏没有 hover,于是永远只是两个色点;② 13pt 命中目标在
|
||||
/// 触屏上太小,而"关闭"误触代价很高;③ 圆点是 mac 窗口的语汇,与本 app 通篇的圆角矩形
|
||||
/// 控件语汇不搭。
|
||||
///
|
||||
/// 现在:22pt 圆角方块 + **常显字形**(`✕` 关闭 / `⌄` 收下去 = 缩回首页卡片),颜色语义保留
|
||||
/// (红=关闭、琥珀=最小化),底色只上 14% 不铺满 —— 与徽标/控件同一套形状语言,命中 26×34。
|
||||
private func windowButton(icon: String, tint: Color, label: String,
|
||||
action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 13, height: 13)
|
||||
.overlay(Circle().strokeBorder(Color.black.opacity(0.25), lineWidth: 0.5))
|
||||
.frame(width: 22, height: 30)
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 9.5, weight: .bold))
|
||||
.foregroundStyle(tint)
|
||||
.frame(width: 22, height: 22)
|
||||
.background(tint.opacity(0.14), in: RoundedRectangle(cornerRadius: 7))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 7).strokeBorder(tint.opacity(0.45), lineWidth: 1)
|
||||
)
|
||||
.frame(width: 26, height: 34)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(.txPressTight)
|
||||
.accessibilityLabel(label)
|
||||
}
|
||||
}
|
||||
@@ -287,6 +313,8 @@ struct TerminalStateOverlay: View {
|
||||
}
|
||||
.padding(TX.Space.l)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// 连上后这一层要淡出去,别"啪"地掀掉(动画由 paneArea 按 phaseText 驱动)。
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +344,7 @@ struct NoTmuxBanner: View {
|
||||
.frame(width: 28, height: 28)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(.txPressTight)
|
||||
}
|
||||
.padding(.horizontal, TX.Space.s)
|
||||
.frame(height: 40)
|
||||
@@ -369,38 +397,74 @@ struct TmuxInstallSheet: View {
|
||||
RoundedRectangle(cornerRadius: TX.Radius.dialog)
|
||||
.strokeBorder(p.borderStrong, lineWidth: 1)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.7), radius: 40, y: 30)
|
||||
.txShadow(TX.Elevation.dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部提示条(34pt):接硬件键盘时只留这条快捷键清单;触控时由键盘附加行替换(UI-5)。
|
||||
struct TerminalHintBar: View {
|
||||
/// 左侧上下文说明(如「Pin 态 · 终端变窄,仅在钉住/解除时 resize 一次」)。
|
||||
/// 舞台底部动作条。
|
||||
///
|
||||
/// **不是快捷键清单,而是真按钮**:iPad 大概率没接键盘,把「竖分屏 / 新 window / 回首页」
|
||||
/// 只写成 `⌘D` 之类的文字提示等于这些功能对触控用户不存在。硬件键盘接上后这几个动作
|
||||
/// 仍然可以走快捷键(UI-5),按钮照旧留着 —— 常驻可点是底线。
|
||||
///
|
||||
/// 高度 = 内容带 34pt + home indicator 安全区,内容在其中**居中** —— 与侧边栏脚、轨道齿轮
|
||||
/// 同一条中心线。(此前只有 34pt 且被安全区顶起,屏幕底下留一条 24pt 的空带,看着像"没铺到底"。)
|
||||
struct TerminalActionBar: View {
|
||||
/// 左侧上下文说明(会话/链路状态一句话)。
|
||||
let context: String
|
||||
let hints: [(key: String, label: String)]
|
||||
let actions: [Action]
|
||||
let safeBottom: CGFloat
|
||||
@EnvironmentObject var theme: TXThemeManager
|
||||
@Environment(\.horizontalSizeClass) private var hSize
|
||||
private var p: TXPalette { theme.palette }
|
||||
|
||||
struct Action: Identifiable {
|
||||
let id: String
|
||||
let icon: String
|
||||
let title: String
|
||||
/// 不可用时灰显但仍在(如非 tmux 会话不能分屏)——不隐藏,免得布局跳。
|
||||
var enabled: Bool = true
|
||||
let run: () -> Void
|
||||
}
|
||||
|
||||
/// 窄屏只留前两个:四个在 iPhone 上放不下,会折行把 34pt 的带撑破。
|
||||
private var shown: [Action] { hSize == .compact ? Array(actions.prefix(2)) : actions }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: TX.Space.s) {
|
||||
Text(context)
|
||||
.font(TX.Font.mono(11.5))
|
||||
.foregroundStyle(p.textWeak)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: TX.Space.m)
|
||||
ForEach(hints, id: \.key) { hint in
|
||||
HStack(spacing: 5) {
|
||||
Text(hint.key)
|
||||
.font(TX.Font.mono(11.5, .medium))
|
||||
.foregroundStyle(p.text4)
|
||||
Text(hint.label)
|
||||
.font(TX.Font.mono(11.5))
|
||||
.foregroundStyle(p.textWeak)
|
||||
Spacer(minLength: TX.Space.s)
|
||||
ForEach(shown) { action in
|
||||
Button(action: action.run) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: action.icon)
|
||||
.font(.system(size: 10.5, weight: .medium))
|
||||
Text(action.title)
|
||||
.font(TX.Font.mono(11.5, .medium))
|
||||
}
|
||||
.foregroundStyle(action.enabled ? p.text2 : p.textFaint)
|
||||
.padding(.horizontal, TX.Space.xs)
|
||||
.frame(height: 26)
|
||||
.background(action.enabled ? p.surface : .clear,
|
||||
in: RoundedRectangle(cornerRadius: TX.Radius.badge))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: TX.Radius.badge)
|
||||
.strokeBorder(action.enabled ? p.border : p.divider, lineWidth: 1)
|
||||
)
|
||||
// 动作条永远**单行**:宁可少放一个也不许折行(折行会溢出底部带)。
|
||||
.lineLimit(1)
|
||||
.fixedSize()
|
||||
}
|
||||
.buttonStyle(.txPress)
|
||||
.disabled(!action.enabled)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, TX.Space.m)
|
||||
.frame(height: TX.Layout.footHeight)
|
||||
.frame(height: TX.Layout.footBand(safeBottom))
|
||||
.animation(TX.Motion.standard, value: context)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user