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

@@ -2,6 +2,57 @@ import SwiftUI
// 稿 / / docs/design/UI-DESIGN-HANDOFF.md
// MARK: -
extension View {
/// `TX.Elevation` `.shadow`
func txShadow(_ s: TX.Shadow) -> some View {
shadow(color: .black.opacity(s.opacity), radius: s.radius, x: s.x, y: s.y)
}
}
/// +
///
/// `.plain` **** ""
/// /
struct TXPressStyle: ButtonStyle {
///
var scale: CGFloat = 0.985
var dim: Double = 0.78
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? scale : 1)
.opacity(configuration.isPressed ? dim : 1)
.animation(TX.Motion.quick, value: configuration.isPressed)
}
}
extension ButtonStyle where Self == TXPressStyle {
/// /
static var txPress: TXPressStyle { TXPressStyle() }
///
static var txPressTight: TXPressStyle { TXPressStyle(scale: 0.9, dim: 0.7) }
}
/// +
///
/// 1. ****`ignoresSafeArea`
/// + SwiftUI
/// `ignoresSafeArea` ****
/// 2. **** 560pt // iPad
///
///
struct TXScreenMetrics<Content: View>: View {
@ViewBuilder let content: (GeometryProxy) -> Content
var body: some View {
GeometryReader { geo in
content(geo)
}
}
}
/// 8pt + 2pt `box-shadow: 0 0 0 2px <>`/
struct TXStatusDot: View {
let color: Color
@@ -18,6 +69,8 @@ struct TXStatusDot: View {
Circle().stroke(punchOut, lineWidth: 2)
}
}
// 绿
.animation(TX.Motion.quick, value: color)
}
}
@@ -55,7 +108,11 @@ struct TXInitialsAvatar: View {
}
}
/// 600 10px, ls .09em, uppercase
/// 600 10px, ls .09em
///
/// ** `.uppercased()`**//`tmux · mac-studio · dev`
/// `TMUX · MAC-STUDIO · DEV`
/// `TXTech`
struct TXSectionLabel: View {
let text: String
/// 3 3
@@ -66,7 +123,7 @@ struct TXSectionLabel: View {
var body: some View {
HStack {
Text(text.uppercased())
Text(text)
.font(TX.Font.mono(10, .semibold))
.tracking(0.9)
.foregroundStyle(color ?? p.textWeak)
@@ -80,25 +137,73 @@ struct TXSectionLabel: View {
}
}
/// mosh / tsnet / SSH / SE key
/// ****`SSH` / `mosh` / `tsnet` / `tmux` /
///
/// **** `SSH``TLS`
/// `mosh` / `tmux` / `tsnet` `macOS``TrueNAS`
/// "" `mosh` `MOSH` `SSH` `ssh`
/// ****
/// `TMUX · MAC-STUDIO · DEV`
///
/// ****"" flavor Catppuccin
/// 12%
@MainActor
enum TXTech {
///
static func transport(_ useMosh: Bool, _ p: TXPalette) -> (text: String, tint: Color) {
useMosh ? ("mosh", p.mosh) : ("SSH", p.linkPath)
}
/// / OS Ubuntu peachDebian red red
///
static func osTint(_ os: String?, _ p: TXPalette) -> Color {
let s = (os ?? "").lowercased()
if s.contains("ubuntu") { return p.reconnecting } // Ubuntu orange
if s.contains("debian") || s.contains("raspberry") { return p.danger }
if s.contains("centos") || s.contains("rhel") || s.contains("red hat") { return p.danger }
if s.contains("fedora") || s.contains("truenas") || s.contains("alpine") { return p.linkPath }
if s.contains("arch") { return p.mosh }
if s.contains("suse") { return p.online }
if s.contains("macos") || s.contains("darwin") { return p.text3 } // Apple
return p.text4
}
}
/// mosh / SSH / tsnet /
///
/// `tint` `TXTech``emphasized` app ** tsnet**
///
///
struct TXBadge: View {
let text: String
var tint: Color?
var emphasized: Bool = false
@EnvironmentObject var theme: TXThemeManager
private var p: TXPalette { theme.palette }
private var fg: Color { emphasized ? TXAccent.text : (tint ?? p.text4) }
private var stroke: Color {
if emphasized { return TXAccent.selectedStroke }
return tint?.opacity(0.35) ?? .clear
}
var body: some View {
Text(text)
.font(TX.Font.mono(11, .medium))
.foregroundStyle(emphasized ? TXAccent.text : p.text4)
.foregroundStyle(fg)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(emphasized ? TXAccent.selectedBg : p.surface,
in: RoundedRectangle(cornerRadius: 5))
.overlay(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(emphasized ? TXAccent.selectedStroke : .clear, lineWidth: 1)
)
.background {
let shape = RoundedRectangle(cornerRadius: 5)
if emphasized {
shape.fill(TXAccent.selectedBg)
} else if let tint {
shape.fill(tint.opacity(0.12))
} else {
shape.fill(p.surface)
}
}
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(stroke, lineWidth: 1))
}
}
@@ -123,8 +228,9 @@ struct TXChip: View {
RoundedRectangle(cornerRadius: TX.Radius.badge)
.strokeBorder(isSelected ? TXAccent.selectedStroke : p.divider, lineWidth: 1)
)
.animation(TX.Motion.quick, value: isSelected)
}
.buttonStyle(.plain)
.buttonStyle(.txPress)
}
}
@@ -134,20 +240,24 @@ struct TXSearchField: View {
var placeholder: String
var shortcut: String?
@EnvironmentObject var theme: TXThemeManager
/// Tab
/// 线
@FocusState private var focused: Bool
private var p: TXPalette { theme.palette }
var body: some View {
HStack(spacing: TX.Space.xs) {
Image(systemName: "magnifyingglass")
.font(.system(size: 12))
.foregroundStyle(p.textWeak)
.foregroundStyle(focused ? TXAccent.text : p.textWeak)
TextField(placeholder, text: $text)
.font(TX.Font.mono(12))
.foregroundStyle(p.text)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
if let shortcut, text.isEmpty {
.focused($focused)
if let shortcut, text.isEmpty, !focused {
Text(shortcut)
.font(TX.Font.mono(10.5))
.foregroundStyle(p.textFaint)
@@ -157,8 +267,10 @@ struct TXSearchField: View {
.frame(height: 32)
.background(p.surface, in: RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8).strokeBorder(p.divider, lineWidth: 1)
RoundedRectangle(cornerRadius: 8)
.strokeBorder(focused ? TXAccent.border : p.divider, lineWidth: 1)
)
.animation(TX.Motion.quick, value: focused)
}
}
@@ -189,7 +301,7 @@ struct TXButton: View {
.strokeBorder(emphasized ? TXAccent.border : p.border, lineWidth: 1)
)
}
.buttonStyle(.plain)
.buttonStyle(.txPress)
}
}
@@ -228,8 +340,9 @@ struct TXIconButton: View {
.frame(width: hitSize ?? max(size, TX.Layout.minTouch),
height: hitSize ?? max(size, TX.Layout.minTouch))
.contentShape(Rectangle())
.animation(TX.Motion.quick, value: isSelected)
}
.buttonStyle(.plain)
.buttonStyle(.txPressTight)
.accessibilityLabel(label ?? icon)
}
}
@@ -251,13 +364,24 @@ struct TXTerminalMirror: View {
var placeholder: String = "等待输出…"
var body: some View {
// **** `Color.clear` overlay overlay
// 20pt
// `.frame(height:)` VStack
// `clipped()`
Color.clear
//
.overlay(alignment: lines.isEmpty ? .center : .bottomLeading) { rows }
.background(background)
.clipped()
}
private var rows: some View {
VStack(alignment: .leading, spacing: 0) {
Spacer(minLength: 0)
if lines.isEmpty {
Text(placeholder)
.font(TX.Font.mono(fontSize))
.foregroundStyle(p.text4)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.frame(maxWidth: .infinity, alignment: .center)
}
ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
Text(line.isEmpty ? " " : line)
@@ -272,9 +396,6 @@ struct TXTerminalMirror: View {
}
.padding(.horizontal, 14)
.padding(.vertical, TX.Space.xs)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.background(background)
.clipped()
}
/// `readViewportText()` ANSI /