feat: UI 商业化改造 + 连接前 tmux 会话选择(真机验证)
按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
241
docs/design/UI-DESIGN-HANDOFF.md
Normal file
241
docs/design/UI-DESIGN-HANDOFF.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Handoff: terminalX — iPadOS 终端 App(沉浸轨道方案)
|
||||
|
||||
## Overview
|
||||
|
||||
terminalX 是面向 iPadOS / iOS 的原生终端运维工具,解决 iPad 远程运维 macOS / Linux 的痛点。技术选型:**libghostty**(终端引擎)· **mosh**(抗断线传输)· **Tailscale tsnet**(用户态入网)· **tmux control mode**(会话复用)。平台优先级 **iPadOS > iOS > macOS(Apple Silicon)**。
|
||||
|
||||
本包描述的是**终端主界面(沉浸轨道)+ 首页(主机管理)+ 设置**这一套已定稿的 UI,含侧边栏三态、分屏模型、关闭/最小化语义、竖屏与 iPhone 降级规则。
|
||||
|
||||
## About the Design Files
|
||||
|
||||
`terminalX iPadOS.dc.html` 是**设计稿**,不是生产代码。它用 HTML/CSS 表达最终视觉与交互意图(1:1 像素尺寸),目的是让你在**目标工程里用原生技术重建**——本项目的目标环境是 **SwiftUI(iPadOS 26 / iOS 26)**,请使用工程已有的架构与组件模式实现,不要移植 HTML/CSS。
|
||||
|
||||
打开方式:浏览器直接打开该文件即可,画板可自由缩放平移。每块屏幕都带 `data-screen-label`,便于对照本文档。
|
||||
|
||||
## Fidelity
|
||||
|
||||
**High-fidelity(hifi)**。颜色、字号、圆角、间距、行高均为最终值,可按本文档的精确数值实现。文案(简体中文,技术术语保留英文)也是最终稿。
|
||||
|
||||
唯一例外:终端内容区的命令输出是**示意数据**,用于验证排版密度与配色,不必照搬。
|
||||
|
||||
---
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### 颜色 — App chrome(Nocturne 设计系统)
|
||||
|
||||
| 用途 | Hex | Nocturne token |
|
||||
| --- | --- | --- |
|
||||
| 画布底 / 屏幕底 | `#161826` | `--color-bg` |
|
||||
| 侧边栏 / 轨道 / 卡片底 | `#1b1d2b` | surface-dark |
|
||||
| 控件底(输入框、次要按钮、徽标) | `#232532` | `--color-surface` |
|
||||
| 舞台底(pane 之间的缝) | `#11121c` | bg-deep |
|
||||
| 分隔线(弱) | `#2c2f3d` | neutral-800 |
|
||||
| 描边(常规) | `#3f424d` | neutral-700 |
|
||||
| 描边(强调 / 对话框) | `#595d6c`、`#4a4d5e` | neutral-600 |
|
||||
| 强调色(唯一) | `#9184d9` | `--color-accent` |
|
||||
| 强调 - 边框 | `#796cbf` | accent-600 |
|
||||
| 强调 - 选中底 | `#2b2741` | accent-900 |
|
||||
| 强调 - 选中描边 | `#423a6a` | accent-800 |
|
||||
| 强调 - 文字 | `#b5abfc` / `#d2cefd` | accent-400 / -300 |
|
||||
| 强调 - 弱化文字 | `#5d5294`、`#968ae0` | accent-700 / -500 |
|
||||
| 主文字 | `#e9e9ed` | `--color-text` |
|
||||
| 次文字 | `#cfd3e5` / `#b2b6ca` | neutral-300 / -400 |
|
||||
| 三级文字 | `#9397ab` | neutral-500 |
|
||||
| 弱文字 / 占位 | `#75798c` | neutral-600 |
|
||||
| 极弱(提示、注脚) | `#4a4d5e` | neutral-700 |
|
||||
|
||||
**规则**:强调色只做线、点、选中底和文字,**绝不大面积铺色**;不使用纯黑纯白。
|
||||
|
||||
### 颜色 — 终端内容区(Catppuccin Mocha,libghostty 渲染)
|
||||
|
||||
`base #1e1e2e`(pane 底)· `mantle #181825` · `surface0 #313244`(vim 状态栏)· `text #cdd6f4` · `subtext #a6adc8` · `overlay #6c7086`(次要输出)· `surface2 #585b70`(禁用/缓存态)
|
||||
语义色:`green #a6e3a1`(成功、提示符)· `red #f38ba8`(error)· `yellow #f9e2af`(warning、git 标记)· `blue #89b4fa`(路径、字段)· `mauve #cba6f7`(关键字)· `teal #94e2d5`(勾选)· `peach #fab387`(重连中)· `pink #f5c2e7`
|
||||
|
||||
设置页可切换 Mocha / Macchiato / Frappé / Latte,**chrome 的中性阶随之推导,强调色恒为 Nocturne blurple `#9184d9`**。
|
||||
|
||||
macOS 窗口按钮沿用系统色:关闭 `#ff5f57`,最小化 `#febc2e`(13pt 圆点,`inset 0 0 0 .5px rgba(0,0,0,.25)`,仅悬停/触摸时显出符号)。
|
||||
|
||||
### 字体
|
||||
|
||||
全局 **JetBrains Mono**(400 / 500 / 600 / 700),chrome 与终端同源。中文走系统回退(PingFang SC)。
|
||||
|
||||
| 场景 | 规格 |
|
||||
| --- | --- |
|
||||
| 屏幕主标题(首页 terminalX) | 500 34px, letter-spacing -.025em |
|
||||
| 页面标题(设置、总览) | 500 24–28px, ls -.02em |
|
||||
| 分区标题 | 500 22px, ls -.02em |
|
||||
| 分组小标题(活动会话 / TMUX) | 600 10px, ls .09em, uppercase, `#75798c` |
|
||||
| 列表主行 | 400–500 13px |
|
||||
| 列表副行 / 元信息 | 400 10.5–11px, `#75798c` |
|
||||
| 徽标 / 胶囊 | 500 11.5–12px |
|
||||
| 终端正文(主 pane) | 400 13.5px / 1.55 |
|
||||
| 终端正文(次 pane / 缩略) | 400 11–12px / 1.5 |
|
||||
| 终端 pane 头 | 500 11px, `#6c7086` |
|
||||
| 底部提示条 | 400 11.5px, `#75798c` |
|
||||
|
||||
### 间距 / 圆角 / 阴影
|
||||
|
||||
- 间距:2 / 6 / 8 / 9 / 10 / 12 / 14 / 16 / 18 / 20 / 26 / 34 / 40(px)
|
||||
- 圆角:徽标 5–7 · 控件 8–9 · 卡片 11–13 · 对话框 14–15 · 屏幕卡 18 · 手机卡 38
|
||||
- 阴影:浮起胶囊 `0 10px 30px rgba(0,0,0,.55)`;侧边栏浮层 `24px 0 60px rgba(0,0,0,.55)`;对话框 `0 30px 80px rgba(0,0,0,.7)`;卡片 `0 6px 18px rgba(0,0,0,.4)`
|
||||
- 遮罩:`rgba(10,11,18,.42–.5)`,浮层用 `backdrop-filter: blur(20px)`
|
||||
|
||||
### 图标
|
||||
|
||||
**Phosphor Icons**(regular)。用到:`magnifying-glass` `house` `plus` `gear-six` `caret-double-left` `caret-double-right` `caret-down` `caret-left` `push-pin` `columns` `terminal-window` `hard-drives` `desktop-tower` `cpu` `key` `shield-check` `lightning` `paint-brush` `text-aa` `keyboard` `export` `info` `warning` `x` `minus` `arrow-right` `arrow-left` `lightbulb` `moon` `check-circle` `wifi-high` `battery-high` `browsers` `rows` `git-branch` `plugs` `trash` `arrows-in-simple` `textbox` `sidebar-simple` `globe-hemisphere-west`。
|
||||
|
||||
SwiftUI 实现请换成等义 **SF Symbols**(例如 `house` / `magnifying-glass` → `house`、`magnifyingglass`;`caret-double-left` → `chevron.compact.left` 或 `sidebar.left`;`push-pin` → `pin`)。
|
||||
|
||||
---
|
||||
|
||||
## Layout 基准
|
||||
|
||||
- 画板尺寸:iPad Pro 13″ 横屏 **1366×1024pt**;竖屏 **1024×1366pt**;iPhone **393×852pt**
|
||||
- 折叠轨道宽 **56pt**(竖屏 48pt),元素中心线统一在 28pt(竖屏 24pt)
|
||||
- 展开侧边栏宽 **288pt**
|
||||
- pane 之间的缝 **2pt**(拖拽热区 **12pt**,把手 44×3pt 圆角 2pt)
|
||||
- 触控命中区不低于 **44pt**(键盘附加行按键高 38–40pt + 6pt 间距 = 44pt 节距)
|
||||
- 终端主 pane 顶部预留 **86pt** 给浮起胶囊(2c 因多一条横幅为 122pt);预留带用 padding-top 实现,内容容器 `min-height:0; overflow:hidden` 防止向上溢出
|
||||
- Home indicator 条:200×4pt(iPhone 140×5pt)
|
||||
|
||||
---
|
||||
|
||||
## Screens / Views
|
||||
|
||||
### 1) 终端主界面 · 沉浸轨道 — `1b 终端-沉浸轨道`
|
||||
|
||||
**Purpose**:真正干活的界面,终端面积最大化。
|
||||
|
||||
**Layout**:`HStack`:左 56pt 轨道 + 右舞台。舞台是 `VStack`:pane 区(flex)+ 硬件键盘提示条 34pt + home 区 14pt。pane 区 padding 2pt,上下两个 pane 比例 1.7 : 1,中间 12pt 拖拽条。
|
||||
|
||||
**轨道自上而下**:Logo 方块(34×34,圆角 11,底 `#232532`,inset 描边 `#3f424d`,字形 `›` 700 16px `#b5abfc`,**不可点**)→ 搜索图标 → 首页图标 → 1pt 分隔 → 主机头像(38×38 圆角 11,两字缩写;选中底 `#2b2741` + inset 描边 `#5d5294`;右上角 8pt 状态点,`box-shadow: 0 0 0 2px #1b1d2b` 挖底)→ 虚线 + 号 → 分隔 → `TMUX` 标签(600 9px ls .1em)+ window 数字(34×30,选中底 `#232532` + inset 描边 `#4a4d5e`)→ `Spacer` → 连接状态点 8pt → 展开键(`caret-double-right`,34×34 底 `#232532` 描边 `#3f424d`)→ 设置齿轮。
|
||||
|
||||
**浮起 chrome**(`position: absolute`,`backdrop-filter: blur(20px)`,底 `rgba(27,29,43,.86–.9)`,描边 `#3f424d`,圆角 11):
|
||||
- 左上标题胶囊 `top 14, left 16`:红/黄圆点 → 1pt 竖分隔 → 6pt 绿点 → 主机名(500 12.5px)→ 「当前 window title · cwd」(400 11.5px `#75798c`)
|
||||
- 右上状态胶囊 `top 14, right 16`:三格分层徽标 `tsnet` | `mosh 41ms` | `tmux dev`,格间 1×16pt 竖线
|
||||
|
||||
**底部提示条**:接硬件键盘时只留 34pt 提示条(快捷键清单);触控时替换为键盘附加行(两行,见 iPhone / 竖屏)。
|
||||
|
||||
### 2) 侧边栏三态 — `3a 侧边栏-遮罩态`、`3a 侧边栏-Pin 态`
|
||||
|
||||
同一条栏的三个状态,**元素不增不减**,只是展示密度不同:
|
||||
|
||||
| 状态 | 宽度 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 折叠 | 56pt | 只显图标与两字缩写 |
|
||||
| 遮罩(默认展开) | 288pt | 面板**盖住**轨道(`left: 0`,不并排,避免两套入口),右侧终端压 `rgba(10,11,18,.5)` 遮罩,**点终端任意处收起**;终端不 reflow,不触发 PTY resize |
|
||||
| Pin | 288pt | 点面板右上 `push-pin` 后钉住:遮罩消失,终端变窄,**只在钉住/解除时各 resize 一次**;再点解除回遮罩态 |
|
||||
|
||||
**开合语义(务必唯一、不歧义)**:`›` 方块是 App logo,不可点。开合只有一枚按钮,两态方向相反——折叠态在轨道底部 `caret-double-right`(展开),展开态在标题右侧 `caret-double-left`(收起)。Pin 是独立按钮,只在展开态出现。快捷键 `⌘\` 开合。
|
||||
|
||||
**面板内容自上而下**:Logo + `terminalX`(500 14px)+ Pin + 收起 → 搜索框(32pt 高,圆角 8)→ **首页**行(`house` + 「首页」+ `⌘⇧H`)→ 分组「活动会话 n」→ 会话行 → 1pt 渐隐分隔 → 分组「TMUX · <主机> · <会话>」→ window 行 + 「新建 window ⌘T」→ `Spacer` → Tailscale 状态脚(绿点 + tailnet + 设备 IP + 齿轮)。
|
||||
|
||||
**行规格(左缘必须对齐)**:会话行 = 26pt 头像列 + 10pt gap;window 行 = 26×22pt 徽标列 + 10pt gap;两者标题左缘同为 **x=53**。选中态用 `box-shadow: inset 0 0 0 1px #423a6a` 而非 `border`,避免选中时内容位移 1pt。
|
||||
|
||||
### 3) 首页 · 主机管理(落地页)— `1e 主机-会话卡`
|
||||
|
||||
**Purpose**:App 冷启动落点;「回到刚才那个会话」是首要动作。
|
||||
|
||||
**Layout**:状态栏 24pt → 页头(terminalX 500 34px + 副行)+ 右侧搜索 220pt / 新建连接 / 设置 → 分组「活动会话」→ **3 列会话缩略卡(flex:1)** → 分组「全部主机」+ 筛选 chips → **4 列主机卡(按内容高度)** → home 区。左右留白 40pt。
|
||||
|
||||
**会话缩略卡**(圆角 13,选中卡描边 `#423a6a` + `0 6px 18px rgba(0,0,0,.4)`):卡头 36pt(状态点 + 主机名 + tmux 会话 + RTT)→ 终端缩略(`#1e1e2e`,11px/1.5,**内容底对齐**,真实回滚)→ 卡脚 38pt(「n windows · 上次活跃」+ `⌘1/2/3` + 主动作「回到会话 →」)。离线卡加 `rgba(24,26,40,.72)` + `blur(1.5px)` 遮罩,内含 26pt 转圈 + 两行说明;**遮罩必须是 pane 的最后一个子节点**(pane 为 `position: relative`)。
|
||||
|
||||
**最小化回来的卡**:置顶并标「最小化中」,转场是从终端缩回该卡的动画。
|
||||
|
||||
### 4) 新建分屏 — `3b 新建分屏菜单`
|
||||
|
||||
**不问方向**(竖分 `⌘D` / 横分 `⌘⇧D` 本来就是两个入口)。菜单只问「分到哪里」:
|
||||
|
||||
- 分组「tmux 会话」:当前会话(带「当前」标签)、其余 detached 会话(title · n windows · 上次活跃)、「新建 tmux 会话…」
|
||||
- 分隔线
|
||||
- 「原生窗口 / 不经 tmux · 关 App 即结束」
|
||||
|
||||
选完**立即分屏**。菜单 360pt 宽,圆角 14,`#1b1d2b` + 描边 `#595d6c`;行高 ~40pt,前导列统一 20pt(状态点也要包在 20pt 居中列里,保证五行文字左缘一致 x=50)。
|
||||
|
||||
**模型约束**:分屏只发生在**同一台主机内**。卡片内部的分割 = 真 tmux pane(`split-window`,服务端持有,断线原样恢复);跨主机分屏**已取消**(层级过深),要同时看两台机器就切会话或 Pin 侧边栏。
|
||||
|
||||
### 5) 关闭 / 最小化 — `3c 关闭确认`
|
||||
|
||||
标题胶囊左侧 macOS 红黄圆点:
|
||||
|
||||
- **黄点(最小化)**:**不确认**,直接回首页,会话继续跑;卡片置顶标「最小化中」。手势:终端下滑。快捷键 `⌘M`。
|
||||
- **红点(关闭)**:**弹一次确认**,按主机能力分两套文案:
|
||||
- tmux 会话 → 主按钮「仅断开 ⏎」(`detach`,服务端继续跑);左侧次要文字按钮「结束会话」(`kill-session`,需再确认)
|
||||
- 原生窗口 → 琥珀警告「没有 tmux 兜底,前台进程会随之结束」;推荐「取消」,危险按钮「关闭窗口」(描边 `#8d5560`,底 `rgba(92,58,63,.35)`,字 `#f5a0ac`)
|
||||
- 快捷键 `⌘W`;`esc` 取消;`⏎` 选推荐项
|
||||
|
||||
对话框 436pt / 404pt 宽,圆角 15。
|
||||
|
||||
### 6) 首页 ⇄ 终端 跳转关系 — `3d 页面跳转关系`
|
||||
|
||||
**进入终端(2 条)**:点活动会话卡 / 会话行 → 恢复到**上一个焦点 pane**;新建连接或主机 + → 建好即进终端,侧边栏同步出现该会话。
|
||||
**离开终端(3 条)**:侧边栏首页行(`⌘⇧H`,会话完全不动)· 黄点最小化(同上但语义是"待会儿还回来")· 红点关闭(二次确认,tmux detach / 原生窗口结束)。
|
||||
换会话不必回首页——侧边栏直接切;回首页表示"换任务"。会话总览层(原 `⌘O` 缩放)**已取消**。
|
||||
|
||||
### 7) 无 tmux 主机 — `2c 无 tmux 主机`
|
||||
|
||||
轨道下半段的 `TMUX` 段换成「窗口」段(客户端窗口,每个 = 一条独立 SSH channel / mosh 会话,可随意新建)。状态胶囊第三格写「无 tmux · 客户端窗口」而非报错。终端顶部一条可关闭的行内横幅:说明窗口由 App 维持、mosh 兜底,但**关掉 App 不会在服务端继续跑**,右侧给「安装并启用 tmux」次要按钮。不弹窗、不强推。
|
||||
|
||||
### 8) 竖屏与 iPhone — `2d iPad 竖屏`、`2d iPhone 竖屏`、`1h iPhone-终端`、`1h iPhone-主机`
|
||||
|
||||
- **iPad 竖屏(1024×1366)**:轨道降到 48pt(元素中心 24pt,同样含搜索/首页/展开键);pane 由左右平铺改为**上下堆叠,最多两层**,多余 pane 折成顶部段控;状态胶囊压缩为「主机名 + RTT」两枚;底部常驻单行键盘附加行。
|
||||
- **iPhone(393×852)**:轨道下沉为**底部主机条**(ms / u2 / nh 胶囊);单 pane 全屏,顶部 3 段进度式 pane 指示 +「左右轻扫切 pane」;下拉标题打开会话列表;键盘附加行两行(esc/ctrl/tab/~///| 与 ↑↓←→/⌘K);主机页用底部 tab(主机 / 会话 / 密钥 / 设置)。
|
||||
|
||||
### 9) 设置 · 外观 — `1g 设置-外观`
|
||||
|
||||
左 280pt 类别栏(外观与主题 / 终端与字体 / Tailscale / 密钥与安全 / mosh 与重连 / tmux 集成 / 键盘与手势 / 诊断日志,脚注版本行),右内容区 padding 28/40。内容:标题 + 说明 → 4 张 Catppuccin 主题卡(88pt 预览 + 5 色点 + 名称,选中描边 `#796cbf` + `check-circle`)→ 两栏(左「实时预览」终端框,右「排版」面板:字体、字号 13.5pt、行高 1.50、连字、CJK 等宽补偿、跟随系统深浅色)。
|
||||
|
||||
---
|
||||
|
||||
## Interactions & Behavior
|
||||
|
||||
| 交互 | 行为 |
|
||||
| --- | --- |
|
||||
| 侧边栏开合 | `⌘\` 或开合键;遮罩态点终端收起;Pin 切换推挤/浮层 |
|
||||
| 切会话 | 轨道主机头像 / 侧边栏会话行 / `⌘1…9`;恢复到该会话上一个焦点 pane |
|
||||
| 切 window | 轨道数字 / 侧边栏 window 行 / `⌃⇥` |
|
||||
| 分屏 | `⌘D` 竖分、`⌘⇧D` 横分 → 弹会话选择菜单 → 立即分屏 |
|
||||
| 新建 | `⌘T` 新 tmux window;`⌘⌥T` 原生窗口 |
|
||||
| 调整 pane 比例 | 拖 12pt 分隔条(把手 44×3pt);松手才向 tmux 发 `resize-pane` |
|
||||
| 回首页 | `⌘⇧H` / 侧边栏首页行 |
|
||||
| 最小化 | 黄点 / `⌘M` / 终端下滑;无确认 |
|
||||
| 关闭 | 红点 / `⌘W`;二次确认,`⏎` = 推荐项,`esc` 取消 |
|
||||
| 命令面板 | `⌘K`(模糊匹配主机、会话、动作) |
|
||||
| 硬件键盘 | 检测到即收起附加行,只留 34pt 提示条;拔掉恢复附加行 |
|
||||
| 长按 `⌘` | 显示全部快捷键面板(iPadOS 系统行为) |
|
||||
|
||||
**动效**:状态切换 200–260ms `easeOut`;侧边栏浮层从左滑入 + 遮罩淡入;最小化 = 终端缩回首页卡片(matched geometry);重连转圈 1s 线性循环。
|
||||
|
||||
**状态徽标规则(分层,只染出问题的那层)**:
|
||||
1. `tsnet` — 网络可达;断则本格转灰/琥珀
|
||||
2. `mosh <RTT>` — 数字实时跳动;`≤120ms` 绿 `#a6e3a1`,`120–200ms` 中性,`>200ms` 琥珀 `#fab387`
|
||||
3. `tmux <会话名>` / 「无 tmux · 客户端窗口」
|
||||
|
||||
**重连**:指数退避,上限 30s;卡片/胶囊显示「重连中 · 第 n 次 · ms 后」;屏幕内容本地缓存,恢复后原样接回(目标 <3s)。
|
||||
|
||||
**安全**:host key TOFU 固定在钥匙串;不匹配时**拦截连接**并告警;私钥存 Secure Enclave(不可导出),密码回退默认关闭。
|
||||
|
||||
## State Management
|
||||
|
||||
```
|
||||
AppRoute = .home | .terminal(sessionID)
|
||||
SidebarState = .collapsed | .overlay | .pinned
|
||||
Session { id, host, kind: .tmux(name) | .nativeWindow, windows[], focusedPaneID,
|
||||
transport: .ssh | .sshOverTsnet | .mosh | .moshOverTsnet,
|
||||
link: .direct(rtt) | .relay(derp, rtt) | .reconnecting(attempt, nextIn) | .offline,
|
||||
lastActiveAt, isMinimized }
|
||||
TmuxWindow { index, title, cwd, panes[] } // title 为 shell 默认值时 UI 回退显示 cwd
|
||||
Host { id, name, group, user, addr, port, os, tsnetIP, hasTmux, hostKeyPinnedAt }
|
||||
```
|
||||
|
||||
关键转换:`选择会话 → .terminal`;`首页/最小化 → .home`(会话保留);`关闭 → detach 或 kill → .home`;`Pin → .pinned`(触发一次 PTY resize)。
|
||||
|
||||
## Files
|
||||
|
||||
- `terminalX iPadOS.dc.html` — 全部 13 块屏幕的设计稿(画板,可缩放平移)
|
||||
- `CLAUDE.md` / `README.md`(源项目文档,若已在仓库中则以仓库版本为准)
|
||||
|
||||
## Assets
|
||||
|
||||
无位图资产。图标全部来自 Phosphor(实现时换 SF Symbols),字体 JetBrains Mono(Google Fonts),终端配色 Catppuccin。
|
||||
80
docs/design/render-screens.py
Normal file
80
docs/design/render-screens.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把设计稿画板拆成每屏一个独立 HTML,便于 headless chrome 逐屏截图。"""
|
||||
import re, os, sys
|
||||
from html.parser import HTMLParser
|
||||
|
||||
# python3 docs/design/render-screens.py && \
|
||||
# for f in /tmp/tx-screens/*.html; do "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
||||
# --headless=new --window-size=1366,1024 --screenshot="${f%.html}.png" "file://$f"; done
|
||||
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "terminalX-iPadOS.dc.html")
|
||||
OUT = "/tmp/tx-screens"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
html = open(SRC, encoding="utf-8").read()
|
||||
|
||||
# 提取 <style>…</style>(helmet 内的全局样式)
|
||||
style = "\n".join(re.findall(r"<style>(.*?)</style>", html, re.S))
|
||||
|
||||
VOID = {"br", "img", "hr", "input", "meta", "link", "source", "area", "base", "col", "embed", "param", "track", "wbr"}
|
||||
|
||||
class Finder(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=False)
|
||||
self.stack = [] # 当前打开的 tag 名
|
||||
self.capture = None # (label, depth, start_offset)
|
||||
self.results = [] # (label, start, end)
|
||||
|
||||
def cur_off(self):
|
||||
line, col = self.getpos()
|
||||
return self.line_starts[line - 1] + col
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag in VOID:
|
||||
return
|
||||
d = dict(attrs)
|
||||
if self.capture is None and "data-screen-label" in d:
|
||||
self.capture = (d["data-screen-label"], len(self.stack), self.cur_off())
|
||||
self.stack.append(tag)
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
pass
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in VOID:
|
||||
return
|
||||
while self.stack and self.stack[-1] != tag:
|
||||
self.stack.pop()
|
||||
if self.stack:
|
||||
self.stack.pop()
|
||||
if self.capture and len(self.stack) == self.capture[1]:
|
||||
label, _, start = self.capture
|
||||
end = self.cur_off() + len(f"</{tag}>")
|
||||
self.results.append((label, start, end))
|
||||
self.capture = None
|
||||
|
||||
f = Finder()
|
||||
f.line_starts = [0]
|
||||
for line in html.splitlines(keepends=True):
|
||||
f.line_starts.append(f.line_starts[-1] + len(line))
|
||||
f.feed(html)
|
||||
|
||||
HEAD = """<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap">
|
||||
<style>%s
|
||||
html,body{margin:0;padding:0;background:#11121c}</style></head><body>"""
|
||||
|
||||
manifest = []
|
||||
for i, (label, s, e) in enumerate(f.results):
|
||||
frag = html[s:e]
|
||||
m = re.search(r"width:(\d+)px;height:(\d+)px", frag)
|
||||
w, h = (m.group(1), m.group(2)) if m else ("1366", "1024")
|
||||
slug = re.sub(r"[^\w一-鿿]+", "_", label).strip("_")
|
||||
path = f"{OUT}/{i:02d}_{slug}.html"
|
||||
open(path, "w", encoding="utf-8").write(HEAD % style + frag + "</body></html>")
|
||||
manifest.append((path, w, h, label))
|
||||
print(f"{i:02d}\t{label}\t{w}x{h}\t{path}")
|
||||
|
||||
with open(f"{OUT}/manifest.tsv", "w") as fh:
|
||||
for p, w, h, l in manifest:
|
||||
fh.write(f"{p}\t{w}\t{h}\t{l}\n")
|
||||
print(f"total {len(manifest)}")
|
||||
1911
docs/design/support.js
Normal file
1911
docs/design/support.js
Normal file
File diff suppressed because it is too large
Load Diff
810
docs/design/terminalX-iPadOS.dc.html
Normal file
810
docs/design/terminalX-iPadOS.dc.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user