Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7795aaf5b0 | ||
|
|
1e838ff56f | ||
|
|
bad9c8552f | ||
|
|
0f668ae9f3 | ||
|
|
f145d84549 | ||
|
|
7e14ba30f9 | ||
|
|
55a76cc51a | ||
|
|
fa1a6cc3af | ||
|
|
b646b46365 | ||
|
|
e951a24889 | ||
|
|
67ecb24ffc | ||
|
|
9f9899b1d1 | ||
|
|
45eeb10283 | ||
|
|
be055673f1 | ||
|
|
b6447443c5 | ||
|
|
ad7c30393c | ||
|
|
ee9e052586 | ||
|
|
5408f27902 | ||
|
|
b056f7c1e4 |
@@ -32,7 +32,6 @@ workspace/
|
|||||||
|
|
||||||
# Local JDK
|
# Local JDK
|
||||||
zulu8.94.0.17-ca-jdk8.0.492-macosx_x64/
|
zulu8.94.0.17-ca-jdk8.0.492-macosx_x64/
|
||||||
zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64/
|
|
||||||
|
|
||||||
# E2E 截图
|
# E2E 截图
|
||||||
**/.e2e-shots/
|
**/.e2e-shots/
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
# macTFS
|
|
||||||
|
|
||||||
> 🤖 **本项目由 Claude(Anthropic AI)生成**:从产品需求、前后端代码,到这份 README 和界面截图,均为纯 AI 生成的实验性作品。
|
|
||||||
>
|
|
||||||
> 🤖 _Built entirely by Claude (Anthropic's AI). Requirements, code, this README, and screenshots are all AI-generated._
|
|
||||||
|
|
||||||
> macOS 上的 TFS(Azure DevOps Server)版本控制客户端 —— 脱离 IDE 宿主,独立运行的桌面应用 + 本地 HTTP API。
|
|
||||||
|
|
||||||
macTFS 让你在 **macOS(含 Apple Silicon)** 上直接对 **TFS / TFVC** 仓库做日常版本控制:获取最新、签出、签入、查看历史、目录与文件对比……不再依赖 Windows + Visual Studio,也不用为了一个 TFS 插件背着一整个 IntelliJ。
|
|
||||||
|
|
||||||
它由一个 **Electron 桌面前端** 和一个 **常驻本地 Java 服务** 组成。Java 服务复用微软官方的 TFS Java SDK 与 TFS 通信,并把能力封装成 `127.0.0.1` 上的 REST API —— 所以除了图形界面,你也可以用 `curl` / 脚本 / CI 直接调用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 截图
|
|
||||||
|
|
||||||
> 以下为演示数据,非真实服务器内容。
|
|
||||||
|
|
||||||
### 连接 TFS
|
|
||||||
|
|
||||||
| 填写连接信息 | 选择 Collection |
|
|
||||||
| :---: | :---: |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
### 工作台
|
|
||||||
|
|
||||||
三栏布局:左侧服务端目录树、中间文件列表、右侧挂起更改与签入、底部操作日志。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 对比与 Diff
|
|
||||||
|
|
||||||
| 目录对比 | 文件 Diff(Monaco) |
|
|
||||||
| :---: | :---: |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 为什么做它
|
|
||||||
|
|
||||||
- **macOS 上几乎没有顺手的 TFVC 客户端。** 微软官方的 Team Explorer Everywhere 只剩命令行 / 老 Eclipse 插件且基本停更;IntelliJ 的 TFS 插件又必须绑着 IDE。
|
|
||||||
- **想要一个能独立运行、界面现代、对 Apple Silicon 友好的桌面工具。** 这就是 macTFS 的目标——为自己/小团队打造的「天天能用」的工具。
|
|
||||||
- **顺带提供 HTTP API。** 让 TFS 操作能被脚本和 CI 复用,而不只是点界面。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心功能
|
|
||||||
|
|
||||||
- **连接与认证**:配置 TFS 服务器地址、NTLM 账号密码,测试连接、保持会话。
|
|
||||||
- **Collection / Workspace / Mapping**:选择项目集合,自动确保默认工作区,集中管理服务端路径 ↔ 本地路径映射。
|
|
||||||
- **服务端目录浏览**:左侧目录树 + 中间文件列表,同步导航、右键菜单统一动作。
|
|
||||||
- **文件同步**:Get Latest、获取指定版本 / Changeset。
|
|
||||||
- **签出签入闭环**:Checkout / Checkin / Add / Delete / Undo,签入附注释。
|
|
||||||
- **本地变更管理**:扫描并展示 Pending Changes,分组签入。
|
|
||||||
- **历史与对比**:文件 / 路径历史、Changeset 详情、本地 vs 云端 Diff、版本间对比、目录对比。
|
|
||||||
- **macOS 原生观感**:仿 Finder 的工作台、Sheet 风格弹窗与轻量动效。
|
|
||||||
- **操作日志**:底部可折叠控制台实时展示操作与错误。
|
|
||||||
|
|
||||||
> 完整的需求与 API 清单见 [`docs/mactfs-prd.md`](docs/mactfs-prd.md)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────────────────┐
|
|
||||||
│ 消费端:Electron UI │ curl │ 脚本 │ CI Pipeline │
|
|
||||||
└──────────────────────────┬───────────────────────────────┘
|
|
||||||
│ HTTP REST (localhost:38765)
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────────┐
|
|
||||||
│ mactfs-server(Java 8 + SparkJava) │
|
|
||||||
│ HTTP 路由 · 会话管理 · 配置存储 · TFS 核心服务层 │
|
|
||||||
└──────────────────────────┬───────────────────────────────┘
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────────────────────────────────┐
|
|
||||||
│ Microsoft TFS SDK(tfsIntegration/lib) │
|
|
||||||
│ SOAP 通信 · NTLM 认证 · 版本控制 │
|
|
||||||
└──────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
| 层级 | 技术 |
|
|
||||||
| --- | --- |
|
|
||||||
| 前端 | Electron + React 19 + React Router 7 + shadcn/ui + Tailwind CSS 4 + Monaco |
|
|
||||||
| 后端 | Java 8 + SparkJava(内嵌 HTTP 服务) |
|
|
||||||
| TFS 内核 | Microsoft TFS Java SDK(SOAP / NTLM) |
|
|
||||||
| 运行环境 | Zulu JDK 8 **x86_64** + Rosetta 2(见下方说明) |
|
|
||||||
|
|
||||||
> ⚠️ **为什么固定 x64 JDK:** TFS SDK 的 JNI 原生库只含 i386/x86_64,没有 arm64 切片。因此服务端 JVM 固定走 x64,在 Apple Silicon 上经 Rosetta 2 运行;Electron 界面本身仍是原生。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境要求
|
|
||||||
|
|
||||||
- macOS 12+(Intel 或 Apple Silicon;Apple Silicon 需安装 Rosetta 2)
|
|
||||||
- [pnpm](https://pnpm.io/)(前端包管理)
|
|
||||||
- 首次构建需联网(自动下载 x64 JDK,见下)
|
|
||||||
- 目标 TFS:TFS 2015 及以下(TFVC)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 克隆
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <你的仓库地址> mactfs
|
|
||||||
cd mactfs/mactfsui
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 准备 x64 JDK(自动)
|
|
||||||
|
|
||||||
服务端需要一个 x64 的 JDK 8。仓库**不**收录这个 200M+ 的二进制,而是按需从 Azul 官方下载并校验:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfsui
|
|
||||||
pnpm prepare:runtime
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本会检测本地是否已有 `zulu8.94.0.17-ca-jdk8.0.492-macosx_x64/`,没有就从 Azul 下载 → 校验 sha256 → 解压到仓库根目录。`pnpm dist` 会自动先跑这一步。
|
|
||||||
|
|
||||||
### 3. 构建后端
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfs
|
|
||||||
../tfsIntegration/gradlew installDist
|
|
||||||
```
|
|
||||||
|
|
||||||
> 后端从 `mactfs/build/install/mactfs/lib/` 加载运行,改了 Java 代码必须重新 `installDist`。
|
|
||||||
|
|
||||||
### 4. 开发模式运行(推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfsui
|
|
||||||
pnpm electron:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
会同时拉起 vite 开发服务器和 Electron 窗口;Electron 发现后端没运行时会自动拉起它,退出时自动回收。
|
|
||||||
|
|
||||||
### 5. 打包成桌面应用(.app / .dmg)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfsui
|
|
||||||
pnpm dist
|
|
||||||
```
|
|
||||||
|
|
||||||
产物输出到 `mactfsui/dist-app/`(universal DMG,内置精简后的 x64 JRE 与服务端 jar)。
|
|
||||||
|
|
||||||
> 启动 / 停止 / 重启 / 排错的完整说明见 [`服务启停指南.md`](服务启停指南.md)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 使用流程
|
|
||||||
|
|
||||||
1. **连接**:填入 TFS 服务器地址、认证方式(NTLM)、账号 / 域 / 密码,点测试连接。
|
|
||||||
2. **选择 Collection**:登录成功后选择要操作的项目集合,系统自动确保默认 Workspace。
|
|
||||||
3. **建立映射**:在目录树或文件列表上「映射到本地」,把服务端路径绑到本地目录(可选立即 Get Latest)。
|
|
||||||
4. **同步文件**:对映射执行 Get Latest,或获取指定版本。
|
|
||||||
5. **改 → 签出 → 签入**:编辑文件后在右侧 Changes 面板查看 Pending Changes,填注释签入。
|
|
||||||
6. **历史 / 对比**:右键查看历史、Changeset 详情,或做本地 vs 云端、版本间、目录级的 Diff。
|
|
||||||
|
|
||||||
### 也可以直接调 API
|
|
||||||
|
|
||||||
服务监听 `127.0.0.1:38765`,token 存在 `~/.mactfs/server-token`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TOKEN=$(cat ~/.mactfs/server-token)
|
|
||||||
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:38765/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
完整 API 清单见 [`docs/mactfs-prd.md`](docs/mactfs-prd.md) 第四章。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
mydev/
|
|
||||||
├── mactfs/ # Java 后端:CLI + 本地 HTTP API 服务
|
|
||||||
├── mactfsui/ # Electron + React 桌面前端
|
|
||||||
│ ├── app/ # 前端源码(页面、组件、API client)
|
|
||||||
│ ├── electron/ # Electron 主进程 / preload
|
|
||||||
│ └── scripts/ # 打包辅助脚本(prepare-runtime.mjs 等)
|
|
||||||
├── tfsIntegration/ # 微软 TFS Java SDK 及原生库(构建依赖)
|
|
||||||
├── docs/ # PRD、需求、截图等文档
|
|
||||||
├── task/ # 分阶段开发任务单
|
|
||||||
└── testing/ # 端到端 / 接口测试报告
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第三方依赖与许可
|
|
||||||
|
|
||||||
- 本项目通过 `tfsIntegration/` 下的 **Microsoft TFS Java SDK** 与 TFS 通信,其分发受微软相应许可约束。
|
|
||||||
- x64 JDK 由 [Azul Zulu](https://www.azul.com/downloads/) 提供,按需下载,不随仓库分发。
|
|
||||||
|
|
||||||
> macTFS 是面向个人 / 小团队的工具型项目,主要解决「在 Mac 上用老 TFS」这一具体痛点。
|
|
||||||
@@ -1,38 +1,50 @@
|
|||||||
# macTFS UI 风格改版需求(macOS 现代化 / 精致化 / 动效)
|
# macTFS UI 风格改版需求(现代桌面工具风 / 精致化 / 动效)
|
||||||
|
|
||||||
## 一、背景
|
## 一、背景
|
||||||
|
|
||||||
macTFS 功能层面已完成 FE-001 ~ FE-025,三栏工作台、弹窗体系、挂起更改与签入链路均可用。但当前 UI 是「功能优先」的默认 shadcn 风格:
|
macTFS 功能层面已完成 FE-001 ~ FE-025,三栏工作台、弹窗体系、挂起更改与签入链路均可用。但当前 UI 是「功能优先」的默认 shadcn 风格:
|
||||||
|
|
||||||
- 全局使用 Roboto Slab 衬线字体,与 macOS 原生观感不符
|
- 全局使用 Roboto Slab 衬线字体,与现代桌面工具观感不符
|
||||||
- 登录页只有一张简单表单卡片,元素少、缺少品牌感
|
- 登录页只有一张简单表单卡片,元素少、缺少品牌感
|
||||||
- 窗口使用系统默认标题栏,没有 macOS 原生质感(毛玻璃、隐藏式标题栏)
|
- 窗口使用系统默认标题栏,应用没有「一体成型」的桌面应用质感
|
||||||
- 几乎没有过渡动效:弹窗、面板折叠、列表加载都是瞬时切换
|
- 几乎没有过渡动效:弹窗、面板折叠、列表加载都是瞬时切换
|
||||||
- 明暗主题虽有 token 定义,但没有切换入口、也不跟随系统
|
- 明暗主题虽有 token 定义,但没有切换入口、也不跟随系统
|
||||||
|
|
||||||
本批需求目标:在**不改任何业务逻辑与接口**的前提下,把整体 UI 升级为「精致、现代、macOS 原生感、富动效」的风格。
|
本批需求目标:在**不改任何业务逻辑与接口**的前提下,把整体 UI 升级为「精致、现代、富动效」的桌面工具风格。
|
||||||
|
|
||||||
> 说明:本分支(`feature/ui-macos-restyle`)做 macOS 原生风方案;姊妹分支 `feature/ui-modern-restyle` 做 Codex/CC Switch 现代工具风方案,两套并行对比。
|
|
||||||
|
|
||||||
## 二、用户诉求原文与解读
|
## 二、用户诉求原文与解读
|
||||||
|
|
||||||
> 「我希望的 ui 风格是更精致,有 macos 风格,动效更多,登录页元素多点,反正就是希望比较现代一些。」
|
> 「我希望的 ui 风格是更精致,动效更多,登录页元素多点,反正就是希望比较现代一些。」
|
||||||
|
>
|
||||||
|
> 补充澄清:「不是 macos 风格 finder 风格,你知道 codex 桌面客户端吗?以及 ccswitch,我想要的是这样的桌面风格。」
|
||||||
|
|
||||||
解读为四个方向:
|
**风格基准定为「现代 AI 开发者工具风」,参照对象:OpenAI Codex 桌面端、CC Switch。**
|
||||||
|
|
||||||
|
这类风格的共同特征(设计基调):
|
||||||
|
|
||||||
|
| 特征 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 中性底色 | 以中性灰(zinc/neutral)分级铺底,内容区与侧栏用明度区分,不用毛玻璃 |
|
||||||
|
| 卡片化 | 信息以圆角卡片承载,细边框(1px 低对比)+ 极轻阴影,hover 时边框/底色微变 |
|
||||||
|
| 克制的主色 | 单一 accent 色只用于主按钮、选中态、链接、状态点,不大面积铺色 |
|
||||||
|
| 留白与排版 | 间距充足、层级靠字号字重区分,无衬线系统字体 |
|
||||||
|
| 隐藏式标题栏 | 无系统标题栏,红绿灯融入应用顶栏,窗口一体成型 |
|
||||||
|
| 明暗双主题 | 默认跟随系统,暗色为深灰而非纯黑 |
|
||||||
|
| 微动效 | hover/按压/出入场都有轻量过渡,快而不花哨(150~250ms) |
|
||||||
|
|
||||||
| 诉求 | 落地方向 |
|
| 诉求 | 落地方向 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 更精致 | 统一设计令牌(颜色/圆角/阴影/间距/图标),细化悬停、选中、禁用等状态 |
|
| 更精致 | 统一设计令牌(颜色/圆角/边框/阴影/间距/图标),细化悬停、选中、禁用态 |
|
||||||
| macOS 风格 | 系统字体栈、隐藏式标题栏 + 毛玻璃、Finder 式侧栏、Sheet 式弹窗、跟随系统明暗 |
|
| 现代桌面风 | 系统字体栈、隐藏式标题栏、卡片化布局、Codex 式侧栏、明暗主题 |
|
||||||
| 动效更多 | 建立统一动效规范:弹窗出入场、面板折叠、列表级联、骨架屏、微交互 |
|
| 动效更多 | 统一动效规范:弹窗出入场、面板折叠、列表级联、骨架屏、微交互 |
|
||||||
| 登录页元素多点 | 品牌区 + 分步表单 + 背景氛围 + 最近连接 + 连接过程动效 |
|
| 登录页元素多点 | 品牌区 + 分步表单 + 氛围背景 + 最近连接 + 连接过程动效 |
|
||||||
|
|
||||||
## 三、现状盘点
|
## 三、现状盘点
|
||||||
|
|
||||||
| 现状 | 文件 | 问题 |
|
| 现状 | 文件 | 问题 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 主题 token | `mactfsui/app/app.css` | 衬线字体、紫色主色、无 macOS 质感变量,dark 主题无入口 |
|
| 主题 token | `mactfsui/app/app.css` | 衬线字体、紫色主色、token 未按中性灰体系组织,dark 主题无入口 |
|
||||||
| Electron 窗口 | `mactfsui/electron/main.cjs` | 默认标题栏,无 vibrancy / hiddenInset |
|
| Electron 窗口 | `mactfsui/electron/main.cjs` | 默认标题栏,应用不「一体成型」 |
|
||||||
| 登录页 | `app/components/app/connect-view.tsx` | 单卡片表单,无品牌区、无动效、无最近连接 |
|
| 登录页 | `app/components/app/connect-view.tsx` | 单卡片表单,无品牌区、无动效、无最近连接 |
|
||||||
| 工作台 | `app/components/app/workspace-shell.tsx`、`top-bar.tsx` | 布局可用但视觉平淡,折叠无动画 |
|
| 工作台 | `app/components/app/workspace-shell.tsx`、`top-bar.tsx` | 布局可用但视觉平淡,折叠无动画 |
|
||||||
| 目录树 / 文件列表 | `app/components/explorer/source-tree-panel.tsx`、`folder-items-panel.tsx` | 行样式简单,加载无骨架屏,空状态简陋 |
|
| 目录树 / 文件列表 | `app/components/explorer/source-tree-panel.tsx`、`folder-items-panel.tsx` | 行样式简单,加载无骨架屏,空状态简陋 |
|
||||||
@@ -43,65 +55,67 @@ macTFS 功能层面已完成 FE-001 ~ FE-025,三栏工作台、弹窗体系、
|
|||||||
|
|
||||||
## 四、需求定义
|
## 四、需求定义
|
||||||
|
|
||||||
### U1 macOS 设计基建(FE-026,P0)
|
### U1 现代桌面风设计基建(FE-026,P0)
|
||||||
|
|
||||||
- 字体切换为系统字体栈(`-apple-system, BlinkMacSystemFont, "SF Pro", "PingFang SC"…`),仅代码/Diff 保留等宽字体
|
- 字体切换为系统无衬线字体栈(`-apple-system, BlinkMacSystemFont, "PingFang SC"…`),仅代码/Diff 保留等宽字体
|
||||||
- 重定义颜色令牌:macOS 蓝为主色(accentColor 风格),中性灰分级、分隔线、半透明材质变量
|
- 重定义颜色令牌:中性灰分级底色(页面底 / 侧栏 / 卡片 / 悬停四档明度)、低对比 1px 边框色、单一克制 accent 色(用于主按钮/选中/状态点)
|
||||||
- 统一圆角(macOS 常用 6/10/14px 档位)、阴影(弹窗/浮层分层阴影)、间距档位
|
- 统一圆角档位(8/10/12px 卡片为主)、极轻阴影(卡片几乎无阴影,浮层/弹窗轻阴影)、间距档位
|
||||||
- 建立动效令牌:统一时长(150/220/320ms)、缓动曲线(ease-out-quart、spring 近似)
|
- 建立动效令牌:统一时长(150/200/250ms)、缓动曲线变量
|
||||||
- 完整 light/dark 两套 token,预留 vibrancy 透明背景变量
|
- 完整 light/dark 两套 token(dark 为深灰底,非纯黑)
|
||||||
|
|
||||||
### U2 Electron 窗口 macOS 化(FE-027,P0)
|
### U2 Electron 无缝窗口与自绘顶栏(FE-027,P0)
|
||||||
|
|
||||||
- `titleBarStyle: "hiddenInset"`,红绿灯融入顶栏,顶栏设置可拖拽区域
|
- `titleBarStyle: "hiddenInset"`,移除系统标题栏,红绿灯融入应用顶栏,校准 trafficLightPosition
|
||||||
- 开启窗口 vibrancy(`under-window` 或 `sidebar` 材质),侧栏呈毛玻璃
|
- 顶栏设置可拖拽区域,交互控件 no-drag
|
||||||
- 窗口最小尺寸、默认尺寸与圆角阴影核对
|
- 窗口背景色与主题 token 一致,启动时无白闪(背景色预设 + ready-to-show)
|
||||||
- 兼容退化:非 macOS 或 vibrancy 不可用时回退为纯色背景
|
- 核对窗口默认尺寸与最小尺寸;非 macOS 平台回退正常标题栏不影响功能
|
||||||
|
|
||||||
### U3 登录页改版(FE-028,P0)
|
### U3 登录页改版(FE-028,P0)
|
||||||
|
|
||||||
- 左右分栏:左侧品牌区(logo、产品名、标语、版本号、氛围背景动效),右侧连接表单
|
- 左右分栏:左侧品牌区(logo、产品名、标语、版本号、低噪点氛围背景:渐变网格 / 微光斑动效),右侧连接表单卡片
|
||||||
- 表单分步过渡动效:连接表单 → Collection 选择,左右滑动 + 淡入淡出
|
- 表单 → Collection 选择两阶段切换增加滑动 + 淡入淡出过渡
|
||||||
- 新增元素:密码可见性切换、记住最近服务器(本地存储多条,可一键填充)、连接状态指示灯、服务健康状态展示
|
- 新增元素:密码可见性切换、最近连接记录(本地存储多条、一键填充)、本地服务健康状态指示灯
|
||||||
- 连接中动效:按钮内 spinner + 步骤文案渐变;失败时表单抖动 + 错误条
|
- 连接中按钮内 spinner + 阶段文案过渡;失败时表单抖动 + 错误提示条
|
||||||
- Collection 选择改为卡片式列表,悬停浮起、选中高亮动画
|
- Collection 选择改卡片式列表:细边框卡片、hover 微浮起、选中 accent 描边动画
|
||||||
|
|
||||||
### U4 工作台外壳与顶栏改版(FE-029,P0)
|
### U4 工作台外壳与顶栏改版(FE-029,P0)
|
||||||
|
|
||||||
- 顶栏融合红绿灯区域,按 macOS 工具栏风格重排(上下文信息居左、动作按钮居右、图标化)
|
- 顶栏融合红绿灯区域,按现代工具风重排:上下文信息居左、动作按钮图标化居右、底部 1px 分隔线
|
||||||
- 左侧目录树面板改 Finder 式侧栏:毛玻璃背景、分组标题、圆角选中条
|
- 左侧目录树面板改 Codex 式侧栏:比内容区深/浅一档的纯色底、圆角选中条、分组标题小字号大写风格
|
||||||
- 三栏折叠/展开增加宽度过渡动画,底部操作台滑入滑出
|
- 左右面板折叠/展开加宽度过渡动画,底部操作台滑入滑出
|
||||||
- 面板间分隔线与留白统一
|
- 三栏分隔线统一为低对比 1px,留白与内边距统一
|
||||||
|
|
||||||
### U5 目录树与文件列表精致化(FE-030,P0)
|
### U5 目录树与文件列表精致化(FE-030,P0)
|
||||||
|
|
||||||
- 树节点与文件行:统一行高、圆角悬停/选中态、展开箭头旋转动画、子级展开渐入
|
- 树节点与文件行:统一行高、圆角悬停/选中态、展开箭头旋转动画、子级展开渐入
|
||||||
- 文件图标体系:按扩展名区分图标与颜色,文件夹/分支/映射目录差异化
|
- 按扩展名区分文件图标与颜色,文件夹/分支/已映射目录差异化
|
||||||
- 加载骨架屏替代文字 loading;空状态配插图式占位(图标 + 引导文案)
|
- 加载骨架屏替代文字 loading;空目录、未连接等空状态配图标 + 引导文案
|
||||||
- 状态徽标(pendingEdit/Add/Delete、未下载等)配色与形状统一
|
- pendingEdit / pendingAdd / pendingDelete / 未下载等状态徽标统一为小圆点 + 浅底色徽标风格
|
||||||
|
- 文件列表首次加载行级联渐入
|
||||||
|
|
||||||
### U6 弹窗体系统一(FE-031,P1)
|
### U6 弹窗体系统一现代风格(FE-031,P1)
|
||||||
|
|
||||||
- 13 个弹窗统一为 macOS Sheet 风格:顶部标题区、内容区、底部按钮区结构一致
|
- 13 个弹窗统一结构:顶部标题区、内容区、底部按钮区,间距与按钮排布一致
|
||||||
- 出入场动效统一:缩放 + 淡入(spring 曲线),遮罩淡入
|
- 卡片化弹窗:圆角、细边框、轻阴影;出入场统一缩放 + 淡入,遮罩淡入
|
||||||
- 弹窗内表格、表单控件按新 token 对齐;大弹窗(History/Compare/Diff)支持更合理的留白与分区
|
- 大弹窗(History/Compare/Diff)优化分区留白与表格样式
|
||||||
|
- 弹窗内表单控件按 U1 token 对齐
|
||||||
|
|
||||||
### U7 操作反馈与微交互(FE-032,P1)
|
### U7 操作反馈与微交互(FE-032,P1)
|
||||||
|
|
||||||
- 引入轻量 toast(成功/失败/进行中),与日志面板互补
|
- 引入轻量 toast(成功/失败/进行中),与日志面板互补
|
||||||
- 挂起更改面板、日志面板按新风格改版;新日志条目滑入动画
|
- 挂起更改面板、日志面板按新风格改版;新日志条目滑入动画
|
||||||
- 按钮、复选框、输入框等控件微交互:按压缩放、聚焦光环、hover 过渡
|
- 控件微交互:按钮按压缩放、聚焦光环、复选框勾选动画、hover 过渡
|
||||||
- 长操作(GetLatest、Checkin、目录对比)显示进度反馈动效
|
- 长操作(GetLatest、Checkin、目录对比)显示进度反馈动效
|
||||||
|
|
||||||
### U8 明暗主题跟随系统(FE-033,P1)
|
### U8 明暗主题跟随系统(FE-033,P1)
|
||||||
|
|
||||||
- 跟随 macOS 系统外观自动切换 light/dark,顶栏提供手动三态开关(跟随/亮/暗)
|
- 跟随系统外观自动切换 light/dark,顶栏手动三态开关(跟随/亮/暗),持久化
|
||||||
- 切换瞬间全局颜色过渡动画
|
- 切换瞬间全局颜色过渡动画
|
||||||
- 校验全部面板与弹窗在 dark 下的对比度与可读性
|
- 校验全部面板与弹窗在 dark 下的对比度与可读性
|
||||||
|
|
||||||
### U9 改版回归验收(FE-034,P0)
|
### U9 改版回归验收(FE-034,P0)
|
||||||
|
|
||||||
- 改版完成后按 `TFS-AI测试流程.md` 大纲做 UI 链路回归:登录、浏览、Mapping、GetLatest、签出签入、历史、Diff、目录对比
|
- 按 `TFS-AI测试流程.md` 大纲做 UI 链路回归:登录、浏览、Mapping、GetLatest、签出签入、历史、Diff、目录对比
|
||||||
- 确认零业务逻辑变化:`app/lib` 与接口契约不动
|
- 确认零业务逻辑变化:`app/lib` 与接口契约不动
|
||||||
- typecheck 通过、关键页面截图存档
|
- typecheck 通过、关键页面截图存档
|
||||||
|
|
||||||
@@ -110,7 +124,7 @@ macTFS 功能层面已完成 FE-001 ~ FE-025,三栏工作台、弹窗体系、
|
|||||||
| 顺序 | 任务 | 优先级 | 依赖 |
|
| 顺序 | 任务 | 优先级 | 依赖 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | FE-026 设计基建 | P0 | 无 |
|
| 1 | FE-026 设计基建 | P0 | 无 |
|
||||||
| 2 | FE-027 窗口 macOS 化 | P0 | FE-026 |
|
| 2 | FE-027 无缝窗口 | P0 | FE-026 |
|
||||||
| 3 | FE-028 登录页改版 | P0 | FE-026 |
|
| 3 | FE-028 登录页改版 | P0 | FE-026 |
|
||||||
| 4 | FE-029 工作台外壳改版 | P0 | FE-026、FE-027 |
|
| 4 | FE-029 工作台外壳改版 | P0 | FE-026、FE-027 |
|
||||||
| 5 | FE-030 目录树与文件列表 | P0 | FE-029 |
|
| 5 | FE-030 目录树与文件列表 | P0 | FE-029 |
|
||||||
@@ -123,5 +137,6 @@ macTFS 功能层面已完成 FE-001 ~ FE-025,三栏工作台、弹窗体系、
|
|||||||
|
|
||||||
- 任何业务逻辑、接口契约、服务端代码改动
|
- 任何业务逻辑、接口契约、服务端代码改动
|
||||||
- 新功能(本批只做视觉与交互层)
|
- 新功能(本批只做视觉与交互层)
|
||||||
|
- 毛玻璃 / vibrancy 材质(已明确不要 Finder 原生风)
|
||||||
- Windows / Linux 适配(仅保证不崩溃的回退样式)
|
- Windows / Linux 适配(仅保证不崩溃的回退样式)
|
||||||
- Monaco Diff 内部主题深度定制(仅跟随明暗主题切换)
|
- Monaco Diff 内部主题深度定制(仅跟随明暗主题切换)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 378 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 490 KiB |
@@ -5,8 +5,3 @@
|
|||||||
# React Router
|
# React Router
|
||||||
/.react-router/
|
/.react-router/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# electron-builder 打包产物与生成的运行时(体积大,不入库)
|
|
||||||
/dist-app/
|
|
||||||
/runtime/
|
|
||||||
/build-resources/icon.iconset/
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
# mactfsui
|
# React Router + shadcn/ui
|
||||||
|
|
||||||
macTFS 的 Electron + React 桌面前端。完整的项目介绍、截图、环境要求与使用说明请见仓库根目录的 [README](../README.md)。
|
This is a template for a new React Router project with React, TypeScript, and shadcn/ui.
|
||||||
|
|
||||||
## 常用命令
|
## Adding components
|
||||||
|
|
||||||
|
To add components to your app, run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install # 安装依赖
|
npx shadcn@latest add button
|
||||||
pnpm electron:dev # 开发模式(vite + Electron,自动拉起后端)
|
|
||||||
pnpm prepare:runtime # 准备 x64 JDK(本地缺失时自动从 Azul 下载)
|
|
||||||
pnpm dist # 打包 macOS 桌面应用到 dist-app/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
启动 / 停止 / 重启 / 排错见根目录的 [`服务启停指南.md`](../服务启停指南.md)。
|
This will place the ui components in the `components` directory.
|
||||||
|
|
||||||
|
## Using components
|
||||||
|
|
||||||
|
To use the components in your app, import them as follows:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
```
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
/* 应用只有亮色主题:保留 class 方案的 dark 变体定义,使组件库模板中残留的
|
|
||||||
dark: 工具类永远不会命中(根节点不再挂 .dark),避免回退到系统媒体查询。 */
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* macOS 原生风设计基建(FE-026)
|
* 现代桌面工具风设计基建(FE-026)
|
||||||
* 基调:系统字体 + macOS 蓝主色 + 中性灰分级 + 半透明材质(vibrancy)变量。
|
* 基调:中性灰分层底色 + 卡片化 + 克制的蓝色主色,参照 Codex 桌面端 / CC Switch。
|
||||||
* 动效约定:快 150ms / 标准 220ms / 慢 320ms,缓动 ease-out-quart / spring 近似。
|
* 动效约定:快 150ms / 标准 200ms / 慢 250ms,对应 duration-150 / duration-200 / duration-250。
|
||||||
*/
|
*/
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||||
--font-heading: var(--font-sans);
|
--font-heading: var(--font-sans);
|
||||||
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
@@ -46,7 +44,7 @@
|
|||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
/* 圆角档位:macOS 常用 6/8/10/14px(--radius = 10px) */
|
/* 圆角档位:sm 6px / md 8px / lg 10px / xl 14px(--radius = 10px) */
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
@@ -54,15 +52,16 @@
|
|||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
/* 阴影分层:卡片极轻 / 浮层轻 / 弹窗(Sheet)明显 */
|
/* 阴影分层:卡片极轻 / 浮层轻 / 弹窗明显 */
|
||||||
--shadow-card: 0 0 0 0.5px rgb(0 0 0 / 0.03), 0 1px 1px rgb(0 0 0 / 0.04), 0 4px 14px rgb(0 0 0 / 0.06);
|
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.04);
|
||||||
--shadow-overlay: 0 4px 16px rgb(0 0 0 / 0.1), 0 1px 3px rgb(0 0 0 / 0.06);
|
--shadow-overlay: 0 4px 16px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.06);
|
||||||
--shadow-dialog: 0 18px 50px rgb(0 0 0 / 0.22), 0 4px 12px rgb(0 0 0 / 0.1);
|
--shadow-dialog: 0 16px 48px rgb(0 0 0 / 0.16), 0 4px 12px rgb(0 0 0 / 0.08);
|
||||||
/* 缓动曲线 */
|
/* 缓动曲线:出场用 out-quart,强调动效用 spring 近似 */
|
||||||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
|
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
|
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
|
||||||
/* 反馈动效:表单错误抖动 */
|
/* 反馈动效:表单错误抖动 / 品牌区光斑缓慢漂浮 */
|
||||||
--animate-shake: shake 0.4s var(--ease-out-quart);
|
--animate-shake: shake 0.4s var(--ease-out-quart);
|
||||||
|
--animate-float-slow: float-slow 9s ease-in-out infinite;
|
||||||
|
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
10%, 90% { transform: translateX(-1px); }
|
10%, 90% { transform: translateX(-1px); }
|
||||||
@@ -70,47 +69,88 @@
|
|||||||
30%, 50%, 70% { transform: translateX(-3px); }
|
30%, 50%, 70% { transform: translateX(-3px); }
|
||||||
40%, 60% { transform: translateX(3px); }
|
40%, 60% { transform: translateX(3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes float-slow {
|
||||||
|
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
||||||
|
50% { transform: translate3d(18px, -22px, 0) scale(1.08); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.98 0.002 286);
|
/* 中性灰分层:页面底 0.985 / 卡片 1 / 侧栏 0.967 / 悬停 0.945 */
|
||||||
--foreground: oklch(0.22 0.006 286);
|
--background: oklch(0.985 0.002 286);
|
||||||
|
--foreground: oklch(0.21 0.006 286);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.22 0.006 286);
|
--card-foreground: oklch(0.21 0.006 286);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.22 0.006 286);
|
--popover-foreground: oklch(0.21 0.006 286);
|
||||||
/* macOS 系统蓝(≈ #007AFF),仅用于主按钮、选中态、状态点 */
|
/* 克制的蓝色主色:仅用于主按钮、选中态、状态点 */
|
||||||
--primary: oklch(0.585 0.205 256.5);
|
--primary: oklch(0.546 0.215 262.881);
|
||||||
--primary-foreground: oklch(0.99 0.002 286);
|
--primary-foreground: oklch(0.985 0.002 286);
|
||||||
--secondary: oklch(0.957 0.004 286);
|
--secondary: oklch(0.96 0.004 286);
|
||||||
--secondary-foreground: oklch(0.22 0.006 286);
|
--secondary-foreground: oklch(0.21 0.006 286);
|
||||||
--muted: oklch(0.957 0.004 286);
|
--muted: oklch(0.96 0.004 286);
|
||||||
--muted-foreground: oklch(0.55 0.012 285);
|
--muted-foreground: oklch(0.552 0.014 285);
|
||||||
/* accent 为中性悬停底色 */
|
/* accent 为中性悬停底色,不再复用主色 */
|
||||||
--accent: oklch(0.94 0.004 286);
|
--accent: oklch(0.945 0.004 286);
|
||||||
--accent-foreground: oklch(0.22 0.006 286);
|
--accent-foreground: oklch(0.21 0.006 286);
|
||||||
--destructive: oklch(0.59 0.24 27.5);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.905 0.004 286);
|
--border: oklch(0.92 0.004 286);
|
||||||
--input: oklch(0.905 0.004 286);
|
--input: oklch(0.92 0.004 286);
|
||||||
--ring: oklch(0.62 0.19 256.5);
|
--ring: oklch(0.623 0.188 259.815);
|
||||||
--chart-1: oklch(0.809 0.105 251.813);
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
--chart-2: oklch(0.7 0.165 254);
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
--chart-3: oklch(0.585 0.205 256.5);
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
--chart-4: oklch(0.5 0.21 259);
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
--chart-5: oklch(0.43 0.19 262);
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
/* 侧栏半透明材质:vibrancy 开启时透出窗口毛玻璃,关闭时回退近实色 */
|
--sidebar: oklch(0.967 0.003 286);
|
||||||
--sidebar: oklch(0.962 0.003 286 / 72%);
|
--sidebar-foreground: oklch(0.21 0.006 286);
|
||||||
--sidebar-foreground: oklch(0.22 0.006 286);
|
--sidebar-primary: oklch(0.546 0.215 262.881);
|
||||||
--sidebar-primary: oklch(0.585 0.205 256.5);
|
--sidebar-primary-foreground: oklch(0.985 0.002 286);
|
||||||
--sidebar-primary-foreground: oklch(0.99 0.002 286);
|
--sidebar-accent: oklch(0.92 0.004 286);
|
||||||
--sidebar-accent: oklch(0.88 0.005 286 / 80%);
|
--sidebar-accent-foreground: oklch(0.21 0.006 286);
|
||||||
--sidebar-border: oklch(0.89 0.004 286);
|
--sidebar-border: oklch(0.91 0.004 286);
|
||||||
--sidebar-ring: oklch(0.62 0.19 256.5);
|
--sidebar-ring: oklch(0.623 0.188 259.815);
|
||||||
/* 动效时长:快 / 标准 / 慢 */
|
/* 动效时长:快 / 标准 / 慢 */
|
||||||
--motion-fast: 150ms;
|
--motion-fast: 150ms;
|
||||||
--motion-base: 220ms;
|
--motion-base: 200ms;
|
||||||
--motion-slow: 320ms;
|
--motion-slow: 250ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* 深灰底而非纯黑:页面 0.185 / 卡片 0.22 / 侧栏 0.165 / 悬停 0.27 */
|
||||||
|
--background: oklch(0.185 0.005 285);
|
||||||
|
--foreground: oklch(0.96 0.002 286);
|
||||||
|
--card: oklch(0.22 0.006 285);
|
||||||
|
--card-foreground: oklch(0.96 0.002 286);
|
||||||
|
--popover: oklch(0.22 0.006 285);
|
||||||
|
--popover-foreground: oklch(0.96 0.002 286);
|
||||||
|
--primary: oklch(0.623 0.188 259.815);
|
||||||
|
--primary-foreground: oklch(0.985 0.002 286);
|
||||||
|
--secondary: oklch(0.27 0.006 286);
|
||||||
|
--secondary-foreground: oklch(0.96 0.002 286);
|
||||||
|
--muted: oklch(0.26 0.006 286);
|
||||||
|
--muted-foreground: oklch(0.71 0.01 286);
|
||||||
|
--accent: oklch(0.27 0.006 286);
|
||||||
|
--accent-foreground: oklch(0.96 0.002 286);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 8%);
|
||||||
|
--input: oklch(1 0 0 / 12%);
|
||||||
|
--ring: oklch(0.623 0.188 259.815);
|
||||||
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
|
--sidebar: oklch(0.165 0.005 285);
|
||||||
|
--sidebar-foreground: oklch(0.96 0.002 286);
|
||||||
|
--sidebar-primary: oklch(0.623 0.188 259.815);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.002 286);
|
||||||
|
--sidebar-accent: oklch(0.24 0.006 285);
|
||||||
|
--sidebar-accent-foreground: oklch(0.96 0.002 286);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 8%);
|
||||||
|
--sidebar-ring: oklch(0.623 0.188 259.815);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -125,15 +165,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* vibrancy 模式(FE-027 在 macOS Electron 下挂载):页面底透明,毛玻璃从半透明侧栏透出 */
|
/* 主题切换瞬间的全局颜色过渡,由 use-theme 短暂挂载 theme-transition class。 */
|
||||||
html.vibrancy,
|
.theme-transition,
|
||||||
html.vibrancy body {
|
.theme-transition * {
|
||||||
background: transparent;
|
transition:
|
||||||
}
|
background-color 250ms var(--ease-out-quart),
|
||||||
|
border-color 250ms var(--ease-out-quart),
|
||||||
/* 工作台画布底色:比卡片深一档的中性灰,让白色卡片浮起;vibrancy 下半透明透出窗口毛玻璃。 */
|
color 250ms var(--ease-out-quart) !important;
|
||||||
@utility canvas-surface {
|
|
||||||
background-color: oklch(0.934 0.004 286 / 0.85);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 隐藏式标题栏下的窗口拖拽区域:顶栏整体可拖动,交互控件单独豁免。 */
|
/* 隐藏式标题栏下的窗口拖拽区域:顶栏整体可拖动,交互控件单独豁免。 */
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const EMPTY_FORM: ConnectFormState = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接入口:连接 TFS、加载并确认 Collection、确保默认 Workspace,成功后回调固定上下文。
|
* 连接入口:连接 TFS、加载并确认 Collection、确保默认 Workspace,成功后回调固定上下文。
|
||||||
* macOS 风:左侧毛玻璃品牌区(大 logo + 一句话),右侧表单,带分阶段过渡与最近连接。
|
* 布局为左品牌区 + 右表单卡片,带分阶段过渡与最近连接快速填充。
|
||||||
*/
|
*/
|
||||||
export function ConnectView({ onReady }: { onReady: (session: WorkspaceSession) => void }) {
|
export function ConnectView({ onReady }: { onReady: (session: WorkspaceSession) => void }) {
|
||||||
const [phase, setPhase] = useState<ConnectPhase>("form")
|
const [phase, setPhase] = useState<ConnectPhase>("form")
|
||||||
@@ -185,21 +185,28 @@ export function ConnectView({ onReady }: { onReady: (session: WorkspaceSession)
|
|||||||
{/* 隐藏式标题栏下登录页的窗口拖拽区 */}
|
{/* 隐藏式标题栏下登录页的窗口拖拽区 */}
|
||||||
<div className="app-drag absolute inset-x-0 top-0 z-10 h-10" />
|
<div className="app-drag absolute inset-x-0 top-0 z-10 h-10" />
|
||||||
|
|
||||||
{/* 左侧品牌区:半透明材质(vibrancy 下呈毛玻璃),大 logo + 一句话 */}
|
{/* 左侧品牌区:大 logo + 一句话描述 */}
|
||||||
<aside className="relative hidden w-[42%] max-w-md min-w-80 flex-col border-r bg-sidebar md:flex">
|
<aside className="relative hidden w-[42%] max-w-md min-w-80 flex-col overflow-hidden bg-zinc-950 p-10 text-zinc-100 md:flex">
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-5 p-10">
|
<div aria-hidden className="pointer-events-none absolute inset-0">
|
||||||
|
<div className="animate-float-slow absolute -top-24 -left-20 size-72 rounded-full bg-blue-500/20 blur-3xl" />
|
||||||
|
<div className="animate-float-slow absolute -right-24 bottom-0 size-80 rounded-full bg-indigo-500/15 blur-3xl [animation-delay:-4s]" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_left,transparent_55%,rgb(9_9_11/0.7))]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative flex flex-1 flex-col items-center justify-center gap-5">
|
||||||
|
{/* logo 自带圆角方形外形,用 drop-shadow 跟随透明轮廓投影 */}
|
||||||
<img src="/logo.png" alt="MacTFS" className="size-24 drop-shadow-2xl" />
|
<img src="/logo.png" alt="MacTFS" className="size-24 drop-shadow-2xl" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-semibold tracking-tight">MacTFS</p>
|
<p className="text-2xl font-semibold tracking-tight">MacTFS</p>
|
||||||
<p className="mt-1.5 text-sm text-muted-foreground">macOS 上的 TFS 源码管理工作台</p>
|
<p className="mt-1.5 text-sm text-zinc-400">macOS 上的 TFS 源码管理工作台</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 p-6 text-xs text-muted-foreground">
|
<div className="relative flex items-center gap-2 text-xs text-zinc-500">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-1.5 rounded-full",
|
"size-1.5 rounded-full",
|
||||||
serviceOk == null ? "bg-muted-foreground/50" : serviceOk ? "bg-emerald-500" : "bg-red-500",
|
serviceOk == null ? "bg-zinc-600" : serviceOk ? "bg-emerald-400" : "bg-red-400",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{serviceOk == null
|
{serviceOk == null
|
||||||
@@ -318,7 +325,7 @@ export function ConnectView({ onReady }: { onReady: (session: WorkspaceSession)
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => applyRecent(item)}
|
onClick={() => applyRecent(item)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
className="flex w-full items-center justify-between gap-2 rounded-md border bg-card px-3 py-2 text-left shadow-card transition-colors duration-150 hover:bg-muted/60"
|
className="flex w-full items-center justify-between gap-2 rounded-md border bg-card px-3 py-2 text-left shadow-card transition-all duration-150 hover:-translate-y-px hover:border-ring/40 hover:shadow-overlay"
|
||||||
>
|
>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block truncate font-mono text-xs">
|
<span className="block truncate font-mono text-xs">
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { FolderCog, PanelBottom, PanelLeft, PanelRight, Unplug } from "lucide-react"
|
import { FolderCog, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Sun, Unplug } from "lucide-react"
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button"
|
import { Button } from "~/components/ui/button"
|
||||||
import { Separator } from "~/components/ui/separator"
|
import { Separator } from "~/components/ui/separator"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"
|
||||||
|
import { useTheme, type ThemePreference } from "~/hooks/use-theme"
|
||||||
import { isMacElectron } from "~/lib/platform"
|
import { isMacElectron } from "~/lib/platform"
|
||||||
import type { WorkspaceSession } from "~/lib/tfs/session"
|
import type { WorkspaceSession } from "~/lib/tfs/session"
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
// 主题三态对应的图标与说明文案。
|
||||||
|
const THEME_VISUALS: Record<ThemePreference, { icon: typeof Monitor; label: string }> = {
|
||||||
|
system: { icon: Monitor, label: "主题:跟随系统" },
|
||||||
|
light: { icon: Sun, label: "主题:亮色" },
|
||||||
|
dark: { icon: Moon, label: "主题:暗色" },
|
||||||
|
}
|
||||||
|
|
||||||
// 三个可折叠面板的显隐状态,由 WorkspaceShell 持有。
|
// 三个可折叠面板的显隐状态,由 WorkspaceShell 持有。
|
||||||
export interface PanelVisibility {
|
export interface PanelVisibility {
|
||||||
tree: boolean
|
tree: boolean
|
||||||
@@ -15,8 +23,8 @@ export interface PanelVisibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顶部上下文栏:仿 Finder 的居中上下文胶囊展示 Collection / Workspace
|
* 顶部上下文栏:展示固定的 Server / Collection / Workspace 上下文,
|
||||||
* (悬停查看服务器地址),右侧为面板折叠开关与重新连接入口,不承载对象级操作。
|
* 以及面板折叠开关与重新连接入口,不承载对象级操作。
|
||||||
*/
|
*/
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
session,
|
session,
|
||||||
@@ -33,6 +41,9 @@ export function TopBar({
|
|||||||
}) {
|
}) {
|
||||||
// 隐藏式标题栏下顶栏整体作为拖拽区,并为左上角红绿灯预留空间。
|
// 隐藏式标题栏下顶栏整体作为拖拽区,并为左上角红绿灯预留空间。
|
||||||
const macInset = isMacElectron()
|
const macInset = isMacElectron()
|
||||||
|
const theme = useTheme()
|
||||||
|
const themeVisual = THEME_VISUALS[theme.preference]
|
||||||
|
const ThemeIcon = themeVisual.icon
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -40,26 +51,20 @@ export function TopBar({
|
|||||||
macInset && "pl-20",
|
macInset && "pl-20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 左侧:程序名 + Collection 胶囊;服务器地址与工作区收进悬停提示 */}
|
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||||
<div className="flex min-w-0 items-center gap-2.5">
|
<span className="shrink-0 font-semibold tracking-tight">MacTFS</span>
|
||||||
<span className="shrink-0 text-sm font-medium">MacTFS</span>
|
<Separator orientation="vertical" className="h-4!" />
|
||||||
<Tooltip>
|
<span className="hidden max-w-56 truncate font-mono text-xs text-muted-foreground lg:inline">
|
||||||
<TooltipTrigger asChild>
|
{session.serverUri}
|
||||||
<div className="app-no-drag flex h-7 min-w-0 items-center rounded-lg bg-foreground/5 px-3 text-xs text-muted-foreground transition-colors hover:bg-foreground/8 hover:text-foreground">
|
</span>
|
||||||
<span className="truncate">{session.collection}</span>
|
<span className="hidden max-w-44 shrink-0 items-center gap-1 truncate rounded-md border bg-card px-2 py-0.5 text-xs font-medium shadow-card sm:inline-flex">
|
||||||
</div>
|
<span className="text-muted-foreground">Collection</span>
|
||||||
</TooltipTrigger>
|
<span className="truncate">{session.collection}</span>
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
</span>
|
||||||
<div className="space-y-0.5">
|
<span className="inline-flex max-w-52 shrink-0 items-center gap-1 truncate rounded-md border bg-card px-2 py-0.5 text-xs shadow-card">
|
||||||
<p>
|
<span className="text-muted-foreground">Workspace</span>
|
||||||
服务器 <span className="font-mono">{session.serverUri}</span>
|
<span className="truncate font-mono">{session.workspace}</span>
|
||||||
</p>
|
</span>
|
||||||
<p>
|
|
||||||
工作区 <span className="font-mono">{session.workspace}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="app-no-drag flex shrink-0 items-center gap-1">
|
<div className="app-no-drag flex shrink-0 items-center gap-1">
|
||||||
@@ -85,6 +90,14 @@ export function TopBar({
|
|||||||
<PanelBottom />
|
<PanelBottom />
|
||||||
</PanelToggle>
|
</PanelToggle>
|
||||||
<Separator orientation="vertical" className="mx-1 h-4!" />
|
<Separator orientation="vertical" className="mx-1 h-4!" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button size="icon-sm" variant="ghost" onClick={theme.cycle} aria-label={themeVisual.label}>
|
||||||
|
<ThemeIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">{themeVisual.label}(点击切换)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button size="icon-sm" variant="ghost" onClick={onManageWorkspace} aria-label="工作区与映射管理">
|
<Button size="icon-sm" variant="ghost" onClick={onManageWorkspace} aria-label="工作区与映射管理">
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export function WorkspaceShell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{/* 毛玻璃画布 + 卡片化面板:半透明画布透出 vibrancy,四个面板为圆角卡片留缝隙 */}
|
{/* 画布式布局:浅灰画布 + 各面板卡片化,卡片间留缝隙避免生硬拼接 */}
|
||||||
<div className="canvas-surface flex h-svh flex-col overflow-hidden">
|
<div className="flex h-svh flex-col overflow-hidden bg-sidebar">
|
||||||
<TopBar
|
<TopBar
|
||||||
session={session}
|
session={session}
|
||||||
panels={panels}
|
panels={panels}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ConflictDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700">
|
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700 dark:text-red-400">
|
||||||
{conflicts.length} 个冲突
|
{conflicts.length} 个冲突
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ function splitDiffSides(raw: string[]): { original: string; modified: string } {
|
|||||||
return { original: original.join("\n"), modified: modified.join("\n") }
|
return { original: original.join("\n"), modified: modified.join("\n") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前应用是否处于暗色主题(class 方案),用于选择 Monaco 主题。
|
||||||
|
*/
|
||||||
|
function isDarkTheme(): boolean {
|
||||||
|
return typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文本 Diff 弹窗:基于 Monaco DiffEditor 渲染,支持 本地 vs 服务器 latest
|
* 文本 Diff 弹窗:基于 Monaco DiffEditor 渲染,支持 本地 vs 服务器 latest
|
||||||
* 与 两个历史版本 对比。提供统一 / 左右分栏视图、上一处 / 下一处差异导航、
|
* 与 两个历史版本 对比。提供统一 / 左右分栏视图、上一处 / 下一处差异导航、
|
||||||
@@ -119,6 +126,7 @@ export function DiffDialog({
|
|||||||
let diffEditor: MonacoEditor.IStandaloneDiffEditor | null = null
|
let diffEditor: MonacoEditor.IStandaloneDiffEditor | null = null
|
||||||
let originalModel: MonacoEditor.ITextModel | null = null
|
let originalModel: MonacoEditor.ITextModel | null = null
|
||||||
let modifiedModel: MonacoEditor.ITextModel | null = null
|
let modifiedModel: MonacoEditor.ITextModel | null = null
|
||||||
|
let themeObserver: MutationObserver | null = null
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const { monaco, detectLanguage } = await import("~/lib/monaco")
|
const { monaco, detectLanguage } = await import("~/lib/monaco")
|
||||||
if (disposed || !containerRef.current) {
|
if (disposed || !containerRef.current) {
|
||||||
@@ -136,7 +144,7 @@ export function DiffDialog({
|
|||||||
// 统一视图下隐藏旧版行号列,避免出现两列紧贴的行号。
|
// 统一视图下隐藏旧版行号列,避免出现两列紧贴的行号。
|
||||||
compactMode: viewMode === "unified",
|
compactMode: viewMode === "unified",
|
||||||
hideUnchangedRegions: { enabled: onlyChanges },
|
hideUnchangedRegions: { enabled: onlyChanges },
|
||||||
theme: "mactfs-light",
|
theme: isDarkTheme() ? "mactfs-dark" : "mactfs-light",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace',
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace',
|
||||||
@@ -160,10 +168,16 @@ export function DiffDialog({
|
|||||||
setBlockCount(diffEditor?.getLineChanges()?.length ?? 0)
|
setBlockCount(diffEditor?.getLineChanges()?.length ?? 0)
|
||||||
})
|
})
|
||||||
editorRef.current = diffEditor
|
editorRef.current = diffEditor
|
||||||
|
// 弹窗打开期间应用切换明暗主题时,同步更新 Monaco 主题。
|
||||||
|
themeObserver = new MutationObserver(() => {
|
||||||
|
monaco.editor.setTheme(isDarkTheme() ? "mactfs-dark" : "mactfs-light")
|
||||||
|
})
|
||||||
|
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
|
||||||
})()
|
})()
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true
|
disposed = true
|
||||||
editorRef.current = null
|
editorRef.current = null
|
||||||
|
themeObserver?.disconnect()
|
||||||
diffEditor?.dispose()
|
diffEditor?.dispose()
|
||||||
originalModel?.dispose()
|
originalModel?.dispose()
|
||||||
modifiedModel?.dispose()
|
modifiedModel?.dispose()
|
||||||
@@ -243,10 +257,10 @@ export function DiffDialog({
|
|||||||
<div className="flex shrink-0 flex-wrap items-center gap-2 text-xs">
|
<div className="flex shrink-0 flex-wrap items-center gap-2 text-xs">
|
||||||
{diff && (
|
{diff && (
|
||||||
<>
|
<>
|
||||||
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700">
|
<Badge variant="secondary" className="rounded-md bg-red-500/10 text-red-700 dark:text-red-400">
|
||||||
− {diff.sourceLabel}
|
− {diff.sourceLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="secondary" className="rounded-md bg-green-500/10 text-green-700">
|
<Badge variant="secondary" className="rounded-md bg-green-500/10 text-green-700 dark:text-green-400">
|
||||||
+ {diff.targetLabel}
|
+ {diff.targetLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-muted-foreground">{changedCount} 行差异</span>
|
<span className="text-muted-foreground">{changedCount} 行差异</span>
|
||||||
|
|||||||
@@ -21,42 +21,42 @@ const FILE_ICON_RULES: { exts: string[]; icon: LucideIcon; className: string }[]
|
|||||||
{
|
{
|
||||||
exts: ["ts", "tsx", "js", "jsx", "java", "cs", "py", "go", "rs", "c", "cpp", "h", "vue", "css", "scss", "html"],
|
exts: ["ts", "tsx", "js", "jsx", "java", "cs", "py", "go", "rs", "c", "cpp", "h", "vue", "css", "scss", "html"],
|
||||||
icon: FileCode,
|
icon: FileCode,
|
||||||
className: "text-blue-600",
|
className: "text-blue-600 dark:text-blue-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["json", "xml", "yml", "yaml", "toml", "config", "properties", "gradle"],
|
exts: ["json", "xml", "yml", "yaml", "toml", "config", "properties", "gradle"],
|
||||||
icon: FileJson,
|
icon: FileJson,
|
||||||
className: "text-amber-600",
|
className: "text-amber-600 dark:text-amber-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["md", "txt", "doc", "docx", "pdf", "rtf"],
|
exts: ["md", "txt", "doc", "docx", "pdf", "rtf"],
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
className: "text-zinc-500",
|
className: "text-zinc-500 dark:text-zinc-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["sql", "db"],
|
exts: ["sql", "db"],
|
||||||
icon: Database,
|
icon: Database,
|
||||||
className: "text-violet-600",
|
className: "text-violet-600 dark:text-violet-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "icns"],
|
exts: ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "icns"],
|
||||||
icon: FileImage,
|
icon: FileImage,
|
||||||
className: "text-emerald-600",
|
className: "text-emerald-600 dark:text-emerald-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["zip", "rar", "7z", "tar", "gz", "jar"],
|
exts: ["zip", "rar", "7z", "tar", "gz", "jar"],
|
||||||
icon: FileArchive,
|
icon: FileArchive,
|
||||||
className: "text-orange-600",
|
className: "text-orange-600 dark:text-orange-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["xls", "xlsx", "csv"],
|
exts: ["xls", "xlsx", "csv"],
|
||||||
icon: FileSpreadsheet,
|
icon: FileSpreadsheet,
|
||||||
className: "text-green-600",
|
className: "text-green-600 dark:text-green-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exts: ["sh", "bat", "cmd", "ps1", "zsh"],
|
exts: ["sh", "bat", "cmd", "ps1", "zsh"],
|
||||||
icon: FileTerminal,
|
icon: FileTerminal,
|
||||||
className: "text-zinc-600",
|
className: "text-zinc-600 dark:text-zinc-300",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export function FileIcon({
|
|||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-3.5 shrink-0",
|
"size-3.5 shrink-0",
|
||||||
mapped ? "text-primary" : "text-sky-600",
|
mapped ? "text-primary" : "text-sky-600 dark:text-sky-400",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export function FolderItemsPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full min-w-0 flex-1 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
<section className="flex h-full min-w-0 flex-1 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
||||||
<div className="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/40 px-3">
|
<div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">当前路径</span>
|
<span className="shrink-0 text-xs text-muted-foreground">当前路径</span>
|
||||||
<span className="min-w-0 truncate font-mono text-xs">{selectedServerPath}</span>
|
<span className="min-w-0 truncate font-mono text-xs">{selectedServerPath}</span>
|
||||||
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
|
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
|
||||||
@@ -172,7 +172,7 @@ export function FolderItemsPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="sticky top-0 z-10 bg-[oklch(0.972_0.003_286)]">
|
<TableHeader className="sticky top-0 z-10 bg-card">
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="h-8 text-xs text-muted-foreground">名称</TableHead>
|
<TableHead className="h-8 text-xs text-muted-foreground">名称</TableHead>
|
||||||
<TableHead className="h-8 w-24 text-xs text-muted-foreground">状态</TableHead>
|
<TableHead className="h-8 w-24 text-xs text-muted-foreground">状态</TableHead>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function SourceTreePanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex h-full w-[280px] shrink-0 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
<aside className="flex h-full w-[280px] shrink-0 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
||||||
<div className="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/40 px-3">
|
<div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
||||||
源码目录
|
源码目录
|
||||||
</span>
|
</span>
|
||||||
@@ -216,7 +216,6 @@ function TreeNode({
|
|||||||
isSelected ? "bg-sidebar-accent" : "hover:bg-sidebar-accent/60",
|
isSelected ? "bg-sidebar-accent" : "hover:bg-sidebar-accent/60",
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 14 + 2}px` }}
|
style={{ paddingLeft: `${depth * 14 + 2}px` }}
|
||||||
title={path}
|
|
||||||
>
|
>
|
||||||
<span className="flex size-5 shrink-0 items-center justify-center">
|
<span className="flex size-5 shrink-0 items-center justify-center">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function WorkspaceManageDialog({
|
|||||||
) : (
|
) : (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-red-500/10 text-red-700"
|
className="bg-red-500/10 text-red-700 dark:text-red-400"
|
||||||
>
|
>
|
||||||
不存在
|
不存在
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function ChangesPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex h-full w-[340px] shrink-0 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
<aside className="flex h-full w-[340px] shrink-0 flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
||||||
<div className="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/40 px-3">
|
<div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
||||||
挂起更改
|
挂起更改
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function ConsolePanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="flex h-full flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
<footer className="flex h-full flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
||||||
<div className="flex h-8 shrink-0 items-center gap-2 border-b bg-muted/40 px-3">
|
<div className="flex h-8 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
<span className="text-[11px] font-medium tracking-wider text-muted-foreground uppercase">
|
||||||
操作日志
|
操作日志
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ease-spring fixed top-12 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 gap-4 rounded-xl border bg-popover p-4 text-sm text-popover-foreground shadow-dialog duration-200 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:slide-in-from-top-6 data-closed:animate-out data-closed:fade-out-0 data-closed:slide-out-to-top-4 data-closed:duration-150 data-closed:ease-out",
|
"ease-out-quart fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border bg-popover p-4 text-sm text-popover-foreground shadow-dialog duration-200 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-open:slide-in-from-top-1 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-closed:duration-150",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ function ScrollArea({
|
|||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
// Radix 默认给内容包一层 display:table + min-width:100% 的 div,会让内容按
|
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||||
// 自身宽度撑开导致 truncate 失效、文字被面板边缘硬切;改为 block 占满视口宽度。
|
|
||||||
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 [&>div]:block! [&>div]:w-full [&>div]:min-w-0!"
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|||||||
<tr
|
<tr
|
||||||
data-slot="table-row"
|
data-slot="table-row"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-primary/8 data-[state=selected]:hover:bg-primary/10",
|
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Check, Loader2, X } from "lucide-react"
|
import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
@@ -70,15 +70,34 @@ export function showToast(input: { id?: string; kind: ToastKind; text: string })
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// toast 类型对应的状态徽标:macOS 通知风的彩色小圆徽(成功绿 / 失败红),loading 用旋转图标。
|
// toast 类型对应的图标、强调条与边框配色:彩色左侧条 + 着色边框提高辨识度。
|
||||||
const KIND_BADGES: Record<"success" | "error", { icon: typeof Check; chipClass: string }> = {
|
const KIND_VISUALS: Record<
|
||||||
success: { icon: Check, chipClass: "bg-emerald-500" },
|
ToastKind,
|
||||||
error: { icon: X, chipClass: "bg-destructive" },
|
{ icon: typeof CheckCircle2; iconClass: string; barClass: string; borderClass: string }
|
||||||
|
> = {
|
||||||
|
success: {
|
||||||
|
icon: CheckCircle2,
|
||||||
|
iconClass: "text-emerald-600 dark:text-emerald-400",
|
||||||
|
barClass: "bg-emerald-500",
|
||||||
|
borderClass: "border-emerald-500/35",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: AlertCircle,
|
||||||
|
iconClass: "text-destructive",
|
||||||
|
barClass: "bg-destructive",
|
||||||
|
borderClass: "border-destructive/40",
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
icon: Loader2,
|
||||||
|
iconClass: "animate-spin text-primary",
|
||||||
|
barClass: "bg-primary",
|
||||||
|
borderClass: "border-primary/35",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* toast 挂载点:固定在窗口右上角(顶栏下方),新条目自上滑入,点击可关闭。
|
* toast 挂载点:固定在窗口右上角(顶栏下方),新条目自上滑入,点击可关闭。
|
||||||
* 视觉对齐 macOS 系统通知:白色毛玻璃圆角卡片 + 彩色状态圆徽 + 柔和投影,无彩色边框。
|
* 彩色强调条 + 着色边框 + 毛玻璃底,保证进度与结果提示足够显眼。
|
||||||
*/
|
*/
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const [items, setItems] = useState<ToastItem[]>([])
|
const [items, setItems] = useState<ToastItem[]>([])
|
||||||
@@ -97,42 +116,26 @@ export function Toaster() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed top-14 right-4 z-[60] flex flex-col items-end gap-2.5">
|
<div className="pointer-events-none fixed top-14 right-3 z-[60] flex w-[380px] flex-col gap-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => {
|
||||||
<button
|
const visual = KIND_VISUALS[item.kind]
|
||||||
key={item.id}
|
const Icon = visual.icon
|
||||||
type="button"
|
return (
|
||||||
onClick={() => dismissToast(item.id)}
|
<button
|
||||||
className="animate-in fade-in slide-in-from-top-2 ease-out-quart pointer-events-auto flex w-fit max-w-[360px] min-w-[200px] items-center gap-2.5 rounded-xl border border-black/8 bg-popover/90 py-2.5 pr-4 pl-3 text-left shadow-[0_8px_24px_rgb(0_0_0/0.12),0_2px_6px_rgb(0_0_0/0.06)] backdrop-blur-xl duration-300"
|
key={item.id}
|
||||||
>
|
type="button"
|
||||||
{item.kind === "loading" ? (
|
onClick={() => dismissToast(item.id)}
|
||||||
<Loader2 className="size-4.5 shrink-0 animate-spin text-primary" />
|
className={cn(
|
||||||
) : (
|
"animate-in fade-in slide-in-from-top-3 ease-out-quart pointer-events-auto relative flex items-start gap-2.5 overflow-hidden rounded-xl border bg-popover/95 py-3 pr-3.5 pl-4 text-left text-sm font-medium shadow-overlay backdrop-blur-md duration-250",
|
||||||
<ToastBadge kind={item.kind} />
|
visual.borderClass,
|
||||||
)}
|
)}
|
||||||
<span className="min-w-0 text-[13px] leading-snug font-medium break-words text-foreground">
|
>
|
||||||
{item.text}
|
<span className={cn("absolute inset-y-0 left-0 w-1", visual.barClass)} />
|
||||||
</span>
|
<Icon className={cn("mt-0.5 size-4.5 shrink-0", visual.iconClass)} />
|
||||||
</button>
|
<span className="min-w-0 break-words">{item.text}</span>
|
||||||
))}
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功 / 失败的彩色圆形徽标:实色圆底 + 白色粗描边图标。
|
|
||||||
*/
|
|
||||||
function ToastBadge({ kind }: { kind: "success" | "error" }) {
|
|
||||||
const badge = KIND_BADGES[kind]
|
|
||||||
const Icon = badge.icon
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"flex size-5 shrink-0 items-center justify-center rounded-full",
|
|
||||||
badge.chipClass,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="size-3 text-white" strokeWidth={3.2} />
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
// 主题偏好:跟随系统 / 强制亮色 / 强制暗色。
|
||||||
|
export type ThemePreference = "system" | "light" | "dark"
|
||||||
|
|
||||||
|
const STORAGE_KEY = "mactfs.theme"
|
||||||
|
const CYCLE_ORDER: ThemePreference[] = ["system", "light", "dark"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前系统外观是否为暗色。
|
||||||
|
*/
|
||||||
|
function systemPrefersDark(): boolean {
|
||||||
|
return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由偏好推导是否应启用暗色。
|
||||||
|
*/
|
||||||
|
function resolveIsDark(preference: ThemePreference): boolean {
|
||||||
|
return preference === "dark" || (preference === "system" && systemPrefersDark())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取持久化的主题偏好,无记录或不可用时默认跟随系统。
|
||||||
|
*/
|
||||||
|
function loadPreference(): ThemePreference {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "system"
|
||||||
|
}
|
||||||
|
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
return raw === "light" || raw === "dark" ? raw : "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用主题到根节点:切换 .dark class,并短暂启用全局颜色过渡动画。
|
||||||
|
*/
|
||||||
|
function applyTheme(preference: ThemePreference) {
|
||||||
|
const root = document.documentElement
|
||||||
|
root.classList.add("theme-transition")
|
||||||
|
root.classList.toggle("dark", resolveIsDark(preference))
|
||||||
|
window.setTimeout(() => root.classList.remove("theme-transition"), 320)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明暗主题 hook:维护「跟随系统 / 亮 / 暗」三态偏好,持久化到 localStorage,
|
||||||
|
* 跟随系统时监听系统外观变化自动切换。
|
||||||
|
*/
|
||||||
|
export function useTheme() {
|
||||||
|
const [preference, setPreferenceState] = useState<ThemePreference>(loadPreference)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(preference)
|
||||||
|
if (preference !== "system") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const media = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
const onChange = () => applyTheme("system")
|
||||||
|
media.addEventListener("change", onChange)
|
||||||
|
return () => media.removeEventListener("change", onChange)
|
||||||
|
}, [preference])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主题偏好并持久化。
|
||||||
|
*/
|
||||||
|
const setPreference = useCallback((next: ThemePreference) => {
|
||||||
|
setPreferenceState(next)
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, next)
|
||||||
|
} catch {
|
||||||
|
// 本地存储不可用时仅在当前会话生效
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 跟随系统 → 亮色 → 暗色 顺序循环切换。
|
||||||
|
*/
|
||||||
|
const cycle = useCallback(() => {
|
||||||
|
const index = CYCLE_ORDER.indexOf(preference)
|
||||||
|
setPreference(CYCLE_ORDER[(index + 1) % CYCLE_ORDER.length])
|
||||||
|
}, [preference, setPreference])
|
||||||
|
|
||||||
|
return { preference, setPreference, cycle }
|
||||||
|
}
|
||||||
@@ -54,6 +54,34 @@ monaco.editor.defineTheme("mactfs-light", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
monaco.editor.defineTheme("mactfs-dark", {
|
||||||
|
base: "vs-dark",
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#1b1b1b",
|
||||||
|
"editor.foreground": "#fafafa",
|
||||||
|
"editor.lineHighlightBackground": "#ffffff08",
|
||||||
|
"editorLineNumber.foreground": "#6b6b6b",
|
||||||
|
"editorLineNumber.activeForeground": "#a3a3a3",
|
||||||
|
"editorCursor.foreground": "#818cf8",
|
||||||
|
"focusBorder": "#00000000",
|
||||||
|
"diffEditor.insertedTextBackground": "#22c55e30",
|
||||||
|
"diffEditor.insertedLineBackground": "#22c55e17",
|
||||||
|
"diffEditor.removedTextBackground": "#ef444430",
|
||||||
|
"diffEditor.removedLineBackground": "#ef444417",
|
||||||
|
"diffEditorGutter.insertedLineBackground": "#22c55e24",
|
||||||
|
"diffEditorGutter.removedLineBackground": "#ef444424",
|
||||||
|
"diffEditorOverview.insertedForeground": "#22c55e99",
|
||||||
|
"diffEditorOverview.removedForeground": "#ef444499",
|
||||||
|
"diffEditor.unchangedRegionBackground": "#222222",
|
||||||
|
"diffEditor.unchangedRegionForeground": "#9c9c9c",
|
||||||
|
"scrollbarSlider.background": "#ffffff1f",
|
||||||
|
"scrollbarSlider.hoverBackground": "#ffffff33",
|
||||||
|
"scrollbarSlider.activeBackground": "#ffffff40",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按文件路径后缀匹配 Monaco 已注册语言,匹配不到时回退纯文本。
|
* 按文件路径后缀匹配 Monaco 已注册语言,匹配不到时回退纯文本。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 是否运行在 macOS 的 Electron 窗口内。
|
* 是否运行在 macOS 的 Electron 窗口内。
|
||||||
* 用于判断隐藏式标题栏与 vibrancy 场景:需要为红绿灯预留空间并启用顶栏拖拽。
|
* 用于判断隐藏式标题栏场景:需要为红绿灯预留空间并启用顶栏拖拽。
|
||||||
*/
|
*/
|
||||||
export function isMacElectron(): boolean {
|
export function isMacElectron(): boolean {
|
||||||
if (typeof navigator === "undefined") {
|
if (typeof navigator === "undefined") {
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ export const STATUS_LABELS: Record<string, string> = {
|
|||||||
|
|
||||||
// 业务状态对应的 badge 配色(Tailwind class),按 Agents.md 状态色规则。
|
// 业务状态对应的 badge 配色(Tailwind class),按 Agents.md 状态色规则。
|
||||||
export const STATUS_BADGE_CLASSES: Record<string, string> = {
|
export const STATUS_BADGE_CLASSES: Record<string, string> = {
|
||||||
localModified: "bg-blue-500/10 text-blue-700",
|
localModified: "bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||||
remoteChanged: "bg-violet-500/10 text-violet-700",
|
remoteChanged: "bg-violet-500/10 text-violet-700 dark:text-violet-400",
|
||||||
bothChanged: "bg-red-500/15 text-red-700",
|
bothChanged: "bg-red-500/15 text-red-700 dark:text-red-400",
|
||||||
localOnly: "bg-green-500/10 text-green-700",
|
localOnly: "bg-green-500/10 text-green-700 dark:text-green-400",
|
||||||
remoteOnly: "bg-violet-500/10 text-violet-700",
|
remoteOnly: "bg-violet-500/10 text-violet-700 dark:text-violet-400",
|
||||||
notDownloaded: "bg-amber-500/10 text-amber-700",
|
notDownloaded: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
|
||||||
localDeleted: "bg-red-500/10 text-red-700",
|
localDeleted: "bg-red-500/10 text-red-700 dark:text-red-400",
|
||||||
pendingEdit: "bg-blue-500/10 text-blue-700",
|
pendingEdit: "bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||||
pendingAdd: "bg-green-500/10 text-green-700",
|
pendingAdd: "bg-green-500/10 text-green-700 dark:text-green-400",
|
||||||
pendingDelete: "bg-red-500/10 text-red-700",
|
pendingDelete: "bg-red-500/10 text-red-700 dark:text-red-400",
|
||||||
pendingRename: "bg-blue-500/10 text-blue-700",
|
pendingRename: "bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||||
pending: "bg-blue-500/10 text-blue-700",
|
pending: "bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||||
upToDate: "bg-muted text-muted-foreground",
|
upToDate: "bg-muted text-muted-foreground",
|
||||||
notMapped: "bg-muted text-muted-foreground",
|
notMapped: "bg-muted text-muted-foreground",
|
||||||
mapped: "bg-muted text-foreground/80",
|
mapped: "bg-muted text-foreground/80",
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
{/* 首屏前标记 vibrancy 环境(macOS Electron 下透出窗口毛玻璃) */}
|
{/* 首屏前同步应用主题偏好,避免暗色模式闪白 */}
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(function(){try{var ua=navigator.userAgent;if(ua.includes("Electron")&&ua.includes("Macintosh"))document.documentElement.classList.add("vibrancy")}catch(e){}})()`,
|
__html: `(function(){try{var t=localStorage.getItem("mactfs.theme");var d=t==="dark"||(t!=="light"&&matchMedia("(prefers-color-scheme: dark)").matches);if(d)document.documentElement.classList.add("dark")}catch(e){}})()`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Meta />
|
<Meta />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell, nativeImage, protocol } = require("electron")
|
const { app, BrowserWindow, ipcMain, dialog, shell, nativeImage, nativeTheme } = require("electron")
|
||||||
const path = require("node:path")
|
const path = require("node:path")
|
||||||
const fs = require("node:fs")
|
const fs = require("node:fs")
|
||||||
const os = require("node:os")
|
const os = require("node:os")
|
||||||
@@ -14,58 +14,6 @@ const HEALTH_TIMEOUT_MS = 2000
|
|||||||
const START_POLL_TIMES = 30
|
const START_POLL_TIMES = 30
|
||||||
const START_POLL_INTERVAL_MS = 1000
|
const START_POLL_INTERVAL_MS = 1000
|
||||||
|
|
||||||
// 自定义协议:以 app://bundle/ 提供前端静态资源。
|
|
||||||
// 打包后构建产物使用绝对路径(/assets/...),用 file:// 会指向磁盘根目录导致白屏,故走自定义协议从 build/client 映射。
|
|
||||||
const APP_SCHEME = "app"
|
|
||||||
const APP_ORIGIN = `${APP_SCHEME}://bundle/`
|
|
||||||
const MIME_BY_EXT = {
|
|
||||||
".html": "text/html",
|
|
||||||
".js": "text/javascript",
|
|
||||||
".mjs": "text/javascript",
|
|
||||||
".css": "text/css",
|
|
||||||
".json": "application/json",
|
|
||||||
".map": "application/json",
|
|
||||||
".ico": "image/x-icon",
|
|
||||||
".png": "image/png",
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".jpeg": "image/jpeg",
|
|
||||||
".gif": "image/gif",
|
|
||||||
".svg": "image/svg+xml",
|
|
||||||
".webp": "image/webp",
|
|
||||||
".ttf": "font/ttf",
|
|
||||||
".woff": "font/woff",
|
|
||||||
".woff2": "font/woff2",
|
|
||||||
".wasm": "application/wasm",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 必须在 app ready 之前注册自定义协议的特权(标准协议 + 安全上下文)。
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
|
||||||
{
|
|
||||||
scheme: APP_SCHEME,
|
|
||||||
privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册 app:// 协议处理器:把请求路径映射到 build/client 下的静态文件,找不到时回退到 index.html(适配前端路由)。
|
|
||||||
*/
|
|
||||||
function registerAppProtocol() {
|
|
||||||
const clientRoot = path.join(__dirname, "..", "build", "client")
|
|
||||||
protocol.handle(APP_SCHEME, async (request) => {
|
|
||||||
let relativePath = decodeURIComponent(new URL(request.url).pathname)
|
|
||||||
if (!relativePath || relativePath === "/") {
|
|
||||||
relativePath = "/index.html"
|
|
||||||
}
|
|
||||||
let filePath = path.normalize(path.join(clientRoot, relativePath))
|
|
||||||
if (!filePath.startsWith(clientRoot) || !fs.existsSync(filePath)) {
|
|
||||||
filePath = path.join(clientRoot, "index.html")
|
|
||||||
}
|
|
||||||
const data = await fs.promises.readFile(filePath)
|
|
||||||
const contentType = MIME_BY_EXT[path.extname(filePath).toLowerCase()] || "application/octet-stream"
|
|
||||||
return new Response(data, { headers: { "content-type": contentType } })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录由本进程拉起的服务端子进程,便于应用退出时回收。
|
// 记录由本进程拉起的服务端子进程,便于应用退出时回收。
|
||||||
let serverProcess = null
|
let serverProcess = null
|
||||||
|
|
||||||
@@ -76,30 +24,19 @@ function resolveProjectRoot() {
|
|||||||
return path.resolve(__dirname, "..", "..")
|
return path.resolve(__dirname, "..", "..")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务端 JVM 固定使用 x64:TFS 的 JNI 原生库(libnative_*.jnilib)只含 i386/x86_64,无 arm64 切片,
|
|
||||||
// 用 arm64 JVM 加载会 UnsatisfiedLinkError。Apple Silicon 上 x64 JVM 经 Rosetta 2 运行;
|
|
||||||
// 服务端是 IO 密集型,Rosetta 开销可接受。UI(Electron)走 universal 原生,已解决界面卡顿。
|
|
||||||
const PACKAGED_JRE_DIR = "jre-x64"
|
|
||||||
const DEV_JDK_DIR = "zulu8.94.0.17-ca-jdk8.0.492-macosx_x64"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析运行服务端使用的 Java 可执行文件:固定 x64 内置 zulu8,其次 JAVA_HOME,最后 PATH 上的 java。
|
* 解析运行服务端使用的 Java 可执行文件,优先项目内置 zulu8,其次 JAVA_HOME,最后 PATH 上的 java。
|
||||||
*/
|
*/
|
||||||
function resolveJavaBin() {
|
function resolveJavaBin() {
|
||||||
// 打包态:x64 JRE 随 extraResources 落在 Resources/jre-x64 下。
|
|
||||||
if (app.isPackaged) {
|
|
||||||
const packaged = path.join(process.resourcesPath, PACKAGED_JRE_DIR, "bin", "java")
|
|
||||||
if (fs.existsSync(packaged)) {
|
|
||||||
return packaged
|
|
||||||
}
|
|
||||||
// 兼容旧单架构包结构(Resources/jre)。
|
|
||||||
const legacy = path.join(process.resourcesPath, "jre", "bin", "java")
|
|
||||||
if (fs.existsSync(legacy)) {
|
|
||||||
return legacy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const projectRoot = resolveProjectRoot()
|
const projectRoot = resolveProjectRoot()
|
||||||
const bundled = path.join(projectRoot, DEV_JDK_DIR, "Contents", "Home", "bin", "java")
|
const bundled = path.join(
|
||||||
|
projectRoot,
|
||||||
|
"zulu8.94.0.17-ca-jdk8.0.492-macosx_x64",
|
||||||
|
"Contents",
|
||||||
|
"Home",
|
||||||
|
"bin",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
if (fs.existsSync(bundled)) {
|
if (fs.existsSync(bundled)) {
|
||||||
return bundled
|
return bundled
|
||||||
}
|
}
|
||||||
@@ -116,13 +53,6 @@ function resolveJavaBin() {
|
|||||||
* 解析服务端 classpath,优先使用 application 安装包 lib 目录(已包含全部运行依赖)。
|
* 解析服务端 classpath,优先使用 application 安装包 lib 目录(已包含全部运行依赖)。
|
||||||
*/
|
*/
|
||||||
function resolveServerClasspath() {
|
function resolveServerClasspath() {
|
||||||
// 打包态:服务端 jar 随 extraResources 落在 Resources/server/lib 下。
|
|
||||||
if (app.isPackaged) {
|
|
||||||
const packagedLib = path.join(process.resourcesPath, "server", "lib")
|
|
||||||
if (fs.existsSync(packagedLib)) {
|
|
||||||
return path.join(packagedLib, "*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const installLib = path.join(
|
const installLib = path.join(
|
||||||
resolveProjectRoot(),
|
resolveProjectRoot(),
|
||||||
"mactfs",
|
"mactfs",
|
||||||
@@ -141,13 +71,6 @@ function resolveServerClasspath() {
|
|||||||
* 解析 TFS native 库目录,供 SDK 加载 JNI 库。
|
* 解析 TFS native 库目录,供 SDK 加载 JNI 库。
|
||||||
*/
|
*/
|
||||||
function resolveNativeDirectory() {
|
function resolveNativeDirectory() {
|
||||||
// 打包态:JNI native 库随服务端 lib 一起落在 Resources/server/lib/native 下。
|
|
||||||
if (app.isPackaged) {
|
|
||||||
const packagedNative = path.join(process.resourcesPath, "server", "lib", "native")
|
|
||||||
if (fs.existsSync(packagedNative)) {
|
|
||||||
return packagedNative
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const projectRoot = resolveProjectRoot()
|
const projectRoot = resolveProjectRoot()
|
||||||
const installNative = path.join(
|
const installNative = path.join(
|
||||||
projectRoot,
|
projectRoot,
|
||||||
@@ -269,7 +192,7 @@ async function startService() {
|
|||||||
}
|
}
|
||||||
args.push(SERVER_MAIN_CLASS)
|
args.push(SERVER_MAIN_CLASS)
|
||||||
serverProcess = spawn(resolveJavaBin(), args, {
|
serverProcess = spawn(resolveJavaBin(), args, {
|
||||||
cwd: app.isPackaged ? process.resourcesPath : path.join(resolveProjectRoot(), "mactfs"),
|
cwd: path.join(resolveProjectRoot(), "mactfs"),
|
||||||
stdio: "ignore",
|
stdio: "ignore",
|
||||||
})
|
})
|
||||||
serverProcess.on("exit", () => {
|
serverProcess.on("exit", () => {
|
||||||
@@ -358,24 +281,18 @@ function resolveAppIcon() {
|
|||||||
*/
|
*/
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const iconPath = resolveAppIcon()
|
const iconPath = resolveAppIcon()
|
||||||
const isMac = process.platform === "darwin"
|
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 840,
|
height: 840,
|
||||||
minWidth: 980,
|
minWidth: 980,
|
||||||
minHeight: 640,
|
minHeight: 640,
|
||||||
title: "MacTFS",
|
title: "MacTFS",
|
||||||
// 预设背景并延迟显示,避免启动白闪;macOS 下背景透明以透出 vibrancy 毛玻璃。
|
// 预设与主题一致的背景色并延迟显示,避免启动白闪。
|
||||||
show: false,
|
show: false,
|
||||||
backgroundColor: isMac ? "#00000000" : "#f9f9fa",
|
backgroundColor: nativeTheme.shouldUseDarkColors ? "#1d1d20" : "#fafafa",
|
||||||
// macOS 隐藏系统标题栏 + 窗口级毛玻璃材质;其它平台回退系统标题栏与实色背景。
|
// macOS 隐藏系统标题栏,红绿灯融入应用顶栏;其它平台回退系统标题栏。
|
||||||
...(isMac
|
...(process.platform === "darwin"
|
||||||
? {
|
? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 16 } }
|
||||||
titleBarStyle: "hiddenInset",
|
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
|
||||||
vibrancy: "sidebar",
|
|
||||||
visualEffectState: "followWindow",
|
|
||||||
}
|
|
||||||
: {}),
|
: {}),
|
||||||
...(iconPath ? { icon: nativeImage.createFromPath(iconPath) } : {}),
|
...(iconPath ? { icon: nativeImage.createFromPath(iconPath) } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -396,8 +313,7 @@ function createWindow() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生产/打包态走自定义协议,保证前端的绝对资源路径(/assets/...)可被正确解析。
|
mainWindow.loadFile(path.join(__dirname, "../build/client/index.html"))
|
||||||
mainWindow.loadURL(APP_ORIGIN)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setName("MacTFS")
|
app.setName("MacTFS")
|
||||||
@@ -408,7 +324,6 @@ app.whenReady().then(() => {
|
|||||||
if (process.platform === "darwin" && iconPath && app.dock) {
|
if (process.platform === "darwin" && iconPath && app.dock) {
|
||||||
app.dock.setIcon(nativeImage.createFromPath(iconPath))
|
app.dock.setIcon(nativeImage.createFromPath(iconPath))
|
||||||
}
|
}
|
||||||
registerAppProtocol()
|
|
||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "mactfsui",
|
"name": "mactfsui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "MacTFS - 面向 macOS 的 TFS 客户端(前端 + 内置 Java 服务)",
|
|
||||||
"author": "mydev",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "electron/main.cjs",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"dev": "react-router dev",
|
"dev": "react-router dev",
|
||||||
"electron": "electron electron/main.cjs",
|
"electron": "electron electron/main.cjs",
|
||||||
"electron:dev": "concurrently -k \"pnpm dev\" \"NO_PROXY=localhost,127.0.0.1 wait-on http://localhost:5173 && NODE_ENV=development electron electron/main.cjs\"",
|
"electron:dev": "concurrently -k \"pnpm dev\" \"NO_PROXY=localhost,127.0.0.1 wait-on http://localhost:5173 && NODE_ENV=development electron electron/main.cjs\"",
|
||||||
"start": "pnpm build && pnpm electron",
|
"start": "pnpm build && pnpm electron",
|
||||||
"prepare:runtime": "node scripts/prepare-runtime.mjs",
|
|
||||||
"dist": "pnpm build && pnpm prepare:runtime && electron-builder --mac --arm64 --x64",
|
|
||||||
"typecheck": "react-router typegen && tsc",
|
"typecheck": "react-router typegen && tsc",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx}\""
|
"format": "prettier --write \"**/*.{ts,tsx}\""
|
||||||
},
|
},
|
||||||
"build": {
|
|
||||||
"appId": "com.mydev.mactfs",
|
|
||||||
"productName": "MacTFS",
|
|
||||||
"directories": {
|
|
||||||
"buildResources": "build-resources",
|
|
||||||
"output": "dist-app"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"electron/**/*",
|
|
||||||
"build/client/**/*",
|
|
||||||
"package.json",
|
|
||||||
"!node_modules/**/*"
|
|
||||||
],
|
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": "../mactfs/build/install/mactfs/lib",
|
|
||||||
"to": "server/lib"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"from": "runtime/jdk-x64",
|
|
||||||
"to": "jre-x64"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mac": {
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "dmg",
|
|
||||||
"arch": [
|
|
||||||
"arm64",
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"category": "public.app-category.developer-tools",
|
|
||||||
"icon": "build-resources/icon.icns",
|
|
||||||
"identity": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "7.13.1",
|
"@react-router/node": "7.13.1",
|
||||||
"@react-router/serve": "7.13.1",
|
"@react-router/serve": "7.13.1",
|
||||||
@@ -79,7 +35,6 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electron": "^42.2.0",
|
"electron": "^42.2.0",
|
||||||
"electron-builder": "^26.15.3",
|
|
||||||
"playwright-core": "^1.60.0",
|
"playwright-core": "^1.60.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// FE-034 清理与诊断:撤销 part2 遗留的 ai-smoke 签出;再从文件列表打开「与最新版本比较」
|
||||||
|
// 复现 Diff 弹窗导致窗口关闭的问题,捕获 renderer 崩溃与主进程输出。
|
||||||
|
import { _electron as electron } from "playwright-core"
|
||||||
|
import { mkdirSync } from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const ROOT = process.cwd()
|
||||||
|
const SHOT_DIR = path.join(ROOT, ".e2e-shots")
|
||||||
|
mkdirSync(SHOT_DIR, { recursive: true })
|
||||||
|
|
||||||
|
const TEST_TREE_CHAIN = ["北京排水集团人力系统", "5SRC", "MicroFront", "subapp_pm", "mactfs"]
|
||||||
|
const SMOKE_FILE = "ai-smoke-20260606051038.txt"
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
function record(step, ok, detail = "") {
|
||||||
|
results.push({ step, ok, detail })
|
||||||
|
console.log(`${ok ? "PASS" : "FAIL"} | ${step}${detail ? " | " + detail : ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shot(win, name) {
|
||||||
|
await win.screenshot({ path: path.join(SHOT_DIR, `${name}.png`) }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await electron.launch({
|
||||||
|
args: ["electron/main.cjs"],
|
||||||
|
cwd: ROOT,
|
||||||
|
env: { ...process.env, NODE_ENV: "development", ELECTRON_ENABLE_LOGGING: "1" },
|
||||||
|
})
|
||||||
|
app.process().stderr?.on("data", (chunk) => {
|
||||||
|
const text = String(chunk)
|
||||||
|
if (/error|crash|fatal/i.test(text)) {
|
||||||
|
console.log("[main-stderr]", text.slice(0, 400))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const win = await app.firstWindow()
|
||||||
|
win.setDefaultTimeout(90000)
|
||||||
|
win.on("crash", () => console.log("[renderer] CRASHED"))
|
||||||
|
win.on("close", () => console.log("[renderer] window closed"))
|
||||||
|
win.on("console", (msg) => {
|
||||||
|
if (msg.type() === "error") {
|
||||||
|
console.log("[console.error]", msg.text().slice(0, 300))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
win.on("pageerror", (err) => console.log("[pageerror]", String(err).slice(0, 300)))
|
||||||
|
|
||||||
|
await win.getByRole("button", { name: "连接 TFS" }).waitFor({ timeout: 90000 })
|
||||||
|
await win.getByRole("button", { name: "连接 TFS" }).click()
|
||||||
|
await win.getByText("选择 Collection").waitFor({ timeout: 120000 })
|
||||||
|
await win.locator("ul button", { hasText: "PKUSEHR" }).first().click()
|
||||||
|
await win.getByRole("button", { name: "进入工作台" }).click()
|
||||||
|
await win.getByText("源码目录").waitFor({ timeout: 120000 })
|
||||||
|
await win.waitForTimeout(5000)
|
||||||
|
|
||||||
|
// 1. 清理:撤销 ai-smoke 的遗留签出(只动这一个文件)
|
||||||
|
const changes = win.locator("aside").nth(1)
|
||||||
|
const smokeRow = changes.getByText(SMOKE_FILE).first()
|
||||||
|
if ((await smokeRow.count()) > 0) {
|
||||||
|
await smokeRow.click({ button: "right" })
|
||||||
|
await win.getByRole("menuitem", { name: "撤销更改", exact: true }).click()
|
||||||
|
await win.waitForTimeout(12000)
|
||||||
|
const after = await changes.innerText()
|
||||||
|
record("1 清理遗留签出", !after.includes(SMOKE_FILE), "")
|
||||||
|
await shot(win, "17-cleanup")
|
||||||
|
} else {
|
||||||
|
record("1 清理遗留签出", true, "无遗留挂起更改")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 诊断:从文件列表打开 与最新版本比较(Monaco Diff)
|
||||||
|
const tree = win.locator("aside").first()
|
||||||
|
for (const name of TEST_TREE_CHAIN) {
|
||||||
|
const node = tree.getByText(name, { exact: true }).first()
|
||||||
|
await node.waitFor({ timeout: 120000 })
|
||||||
|
await node.click()
|
||||||
|
await win.waitForTimeout(2000)
|
||||||
|
}
|
||||||
|
const fileRow = win.locator("section table tbody tr").filter({ hasText: SMOKE_FILE }).first()
|
||||||
|
await fileRow.click({ button: "right" })
|
||||||
|
const diffItem = win.getByRole("menuitem", { name: "与最新版本比较", exact: true })
|
||||||
|
await diffItem.waitFor({ timeout: 10000 })
|
||||||
|
await diffItem.click()
|
||||||
|
await win.waitForTimeout(3000)
|
||||||
|
await shot(win, "18-diff-opening")
|
||||||
|
await win.waitForTimeout(7000)
|
||||||
|
const dialogCount = await win.locator("[data-slot=dialog-content]").count()
|
||||||
|
if (dialogCount > 0) {
|
||||||
|
const diffText = await win.locator("[data-slot=dialog-content]").innerText()
|
||||||
|
record("2 Diff 弹窗(Monaco)", diffText.includes("差异") || diffText.includes("一致"), diffText.slice(0, 80).replace(/\n/g, " "))
|
||||||
|
await shot(win, "18-diff")
|
||||||
|
await win.keyboard.press("Escape")
|
||||||
|
} else {
|
||||||
|
record("2 Diff 弹窗(Monaco)", false, "弹窗未出现")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
record("EXCEPTION", false, String(error).slice(0, 300))
|
||||||
|
try {
|
||||||
|
const win = await app.firstWindow()
|
||||||
|
await shot(win, "97-exception")
|
||||||
|
} catch {}
|
||||||
|
} finally {
|
||||||
|
await app.close().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n==== RESULT SUMMARY ====")
|
||||||
|
for (const item of results) {
|
||||||
|
console.log(`${item.ok ? "✅" : "❌"} ${item.step}${item.detail ? " — " + item.detail : ""}`)
|
||||||
|
}
|
||||||
|
process.exit(results.some((item) => !item.ok) ? 1 : 0)
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { existsSync, rmSync, cpSync, mkdtempSync } from "node:fs"
|
|
||||||
import { execFileSync } from "node:child_process"
|
|
||||||
import { tmpdir } from "node:os"
|
|
||||||
import { join, resolve, dirname } from "node:path"
|
|
||||||
import { fileURLToPath } from "node:url"
|
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url))
|
|
||||||
const projectRoot = resolve(here, "..", "..")
|
|
||||||
|
|
||||||
// 服务端 JVM 固定 x64:TFS 的 JNI 原生库只含 i386/x86_64(无 arm64),arm64 JVM 无法加载。
|
|
||||||
// 因此 universal 包内只随附 x64 JRE,在 Apple Silicon 上经 Rosetta 2 运行服务端。
|
|
||||||
//
|
|
||||||
// JDK 体积大且是第三方二进制,不入库(见 .gitignore)。本地缺失时从 Azul 官方按需下载,
|
|
||||||
// 校验 sha256 后解压到仓库根目录,再继续复制流程。
|
|
||||||
const JDK_DIR_NAME = "zulu8.94.0.17-ca-jdk8.0.492-macosx_x64"
|
|
||||||
const JDK_URL = `https://cdn.azul.com/zulu/bin/${JDK_DIR_NAME}.tar.gz`
|
|
||||||
const JDK_SHA256 =
|
|
||||||
"5114b269b88e3d89b0d6b2c28af0c96b5489f340fbaded8fa17613b2adca180c"
|
|
||||||
|
|
||||||
const source = join(projectRoot, JDK_DIR_NAME, "Contents", "Home")
|
|
||||||
const dest = resolve(here, "..", "runtime", "jdk-x64")
|
|
||||||
|
|
||||||
function ensureJdkPresent() {
|
|
||||||
if (existsSync(join(source, "bin", "java"))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[prepare-runtime] 未找到本地 x64 JDK,开始从 Azul 下载…")
|
|
||||||
console.log("[prepare-runtime] URL:", JDK_URL)
|
|
||||||
|
|
||||||
const workDir = mkdtempSync(join(tmpdir(), "mactfs-jdk-"))
|
|
||||||
const archive = join(workDir, "zulu.tar.gz")
|
|
||||||
try {
|
|
||||||
execFileSync(
|
|
||||||
"curl",
|
|
||||||
["-fsSL", "--retry", "3", "--max-time", "600", "-o", archive, JDK_URL],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
)
|
|
||||||
|
|
||||||
const actualSha = execFileSync("shasum", ["-a", "256", archive])
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)[0]
|
|
||||||
if (actualSha !== JDK_SHA256) {
|
|
||||||
throw new Error(
|
|
||||||
`JDK sha256 校验失败:期望 ${JDK_SHA256},实际 ${actualSha}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
console.log("[prepare-runtime] sha256 校验通过,解压到", projectRoot)
|
|
||||||
|
|
||||||
execFileSync("tar", ["xzf", archive, "-C", projectRoot], {
|
|
||||||
stdio: "inherit",
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
rmSync(workDir, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(join(source, "bin", "java"))) {
|
|
||||||
console.error("[prepare-runtime] 下载解压后仍未找到 x64 JDK:", source)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureJdkPresent()
|
|
||||||
|
|
||||||
console.log("[prepare-runtime] 复制 x64 JDK ->", dest)
|
|
||||||
rmSync(dest, { recursive: true, force: true })
|
|
||||||
cpSync(source, dest, { recursive: true })
|
|
||||||
|
|
||||||
// 运行服务用不到的部分,复制后删除以缩小打包体积(仍保留 bin/java + jre + lib)。
|
|
||||||
const drop = ["src.zip", "demo", "sample", "man", "include"]
|
|
||||||
for (const name of drop) {
|
|
||||||
rmSync(join(dest, name), { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(join(dest, "bin", "java"))) {
|
|
||||||
console.error("[prepare-runtime] 精简后 bin/java 缺失,终止。")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[prepare-runtime] x64 JRE 准备完成:", dest)
|
|
||||||
@@ -38,7 +38,7 @@ blocked
|
|||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
UI 风格改版·macOS 原生风方案完成(分支 `feature/ui-macos-restyle`,FE-026 ~ FE-034 全部 done;姊妹分支 `feature/ui-modern-restyle` 的现代工具风方案也已完成,两套可并行对比体验,截图见 [docs/ui-restyle-screenshots](/Users/fenghp/Desktop/DEV/project/mydev/docs/ui-restyle-screenshots))
|
UI 风格改版完成(分支 `feature/ui-modern-restyle`,FE-026 ~ FE-034 全部 done,需求见 [docs/mactfs-ui-restyle-requirements.md](/Users/fenghp/Desktop/DEV/project/mydev/docs/mactfs-ui-restyle-requirements.md);截图见 [docs/ui-restyle-screenshots](/Users/fenghp/Desktop/DEV/project/mydev/docs/ui-restyle-screenshots))
|
||||||
|
|
||||||
## 前端规范
|
## 前端规范
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ UI 风格改版·macOS 原生风方案完成(分支 `feature/ui-macos-restyle`
|
|||||||
|
|
||||||
## 下一步任务
|
## 下一步任务
|
||||||
|
|
||||||
- 两个 UI 分支对比体验后选定方案
|
|
||||||
- [FEATURE-001-连接与目录浏览端到端验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/feature/FEATURE-001-连接与目录浏览端到端验收.md)(需要真实 TFS 环境)
|
- [FEATURE-001-连接与目录浏览端到端验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/feature/FEATURE-001-连接与目录浏览端到端验收.md)(需要真实 TFS 环境)
|
||||||
|
- 或切换 `feature/ui-macos-restyle` 分支开发 macOS 原生风方案做对比
|
||||||
- UI 改版完成后再回到 [FEATURE-001-连接与目录浏览端到端验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/feature/FEATURE-001-连接与目录浏览端到端验收.md)(需要真实 TFS 环境)
|
- UI 改版完成后再回到 [FEATURE-001-连接与目录浏览端到端验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/feature/FEATURE-001-连接与目录浏览端到端验收.md)(需要真实 TFS 环境)
|
||||||
|
|
||||||
## Core
|
## Core
|
||||||
@@ -118,12 +118,12 @@ UI 风格改版·macOS 原生风方案完成(分支 `feature/ui-macos-restyle`
|
|||||||
| [FE-023-实现分支.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-023-实现分支.md) | done | P1 |
|
| [FE-023-实现分支.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-023-实现分支.md) | done | P1 |
|
||||||
| [FE-024-实现合并.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-024-实现合并.md) | done | P1 |
|
| [FE-024-实现合并.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-024-实现合并.md) | done | P1 |
|
||||||
| [FE-025-实现访达打开与添加本地文件.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-025-实现访达打开与添加本地文件.md) | done | P0 |
|
| [FE-025-实现访达打开与添加本地文件.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-025-实现访达打开与添加本地文件.md) | done | P0 |
|
||||||
| [FE-026-建立macOS设计基建.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-026-建立macOS设计基建.md) | done | P0 |
|
| [FE-026-建立现代桌面风设计基建.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-026-建立现代桌面风设计基建.md) | done | P0 |
|
||||||
| [FE-027-Electron窗口macOS化.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-027-Electron窗口macOS化.md) | done | P0 |
|
| [FE-027-Electron无缝窗口与自绘顶栏.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-027-Electron无缝窗口与自绘顶栏.md) | done | P0 |
|
||||||
| [FE-028-重设计登录页.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-028-重设计登录页.md) | done | P0 |
|
| [FE-028-重设计登录页.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-028-重设计登录页.md) | done | P0 |
|
||||||
| [FE-029-工作台外壳与顶栏改版.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-029-工作台外壳与顶栏改版.md) | done | P0 |
|
| [FE-029-工作台外壳与顶栏改版.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-029-工作台外壳与顶栏改版.md) | done | P0 |
|
||||||
| [FE-030-目录树与文件列表精致化.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-030-目录树与文件列表精致化.md) | done | P0 |
|
| [FE-030-目录树与文件列表精致化.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-030-目录树与文件列表精致化.md) | done | P0 |
|
||||||
| [FE-031-弹窗体系统一macOS风格.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-031-弹窗体系统一macOS风格.md) | done | P1 |
|
| [FE-031-弹窗体系统一现代风格.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-031-弹窗体系统一现代风格.md) | done | P1 |
|
||||||
| [FE-032-操作反馈与微交互.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-032-操作反馈与微交互.md) | done | P1 |
|
| [FE-032-操作反馈与微交互.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-032-操作反馈与微交互.md) | done | P1 |
|
||||||
| [FE-033-明暗主题跟随系统.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-033-明暗主题跟随系统.md) | done | P1 |
|
| [FE-033-明暗主题跟随系统.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-033-明暗主题跟随系统.md) | done | P1 |
|
||||||
| [FE-034-UI改版回归验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-034-UI改版回归验收.md) | done | P0 |
|
| [FE-034-UI改版回归验收.md](/Users/fenghp/Desktop/DEV/project/mydev/task/frontend/FE-034-UI改版回归验收.md) | done | P0 |
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# FE-026 建立macOS设计基建
|
|
||||||
|
|
||||||
## 状态
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
||||||
## 优先级
|
|
||||||
|
|
||||||
P0
|
|
||||||
|
|
||||||
## 所属阶段
|
|
||||||
|
|
||||||
frontend
|
|
||||||
|
|
||||||
## 依赖任务
|
|
||||||
|
|
||||||
无
|
|
||||||
|
|
||||||
## 需求来源
|
|
||||||
|
|
||||||
- [docs/mactfs-ui-restyle-requirements.md](/Users/fenghp/Desktop/DEV/project/mydev/docs/mactfs-ui-restyle-requirements.md) U1
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
把全局设计令牌升级为 macOS 现代风格,作为后续所有 UI 改版任务的统一基础。
|
|
||||||
|
|
||||||
## 实现范围
|
|
||||||
|
|
||||||
- 字体切换为系统字体栈(`-apple-system, BlinkMacSystemFont, "PingFang SC"` 等),移除 Roboto Slab 作为全局字体;代码/Diff 场景保留等宽字体
|
|
||||||
- 重定义颜色令牌:macOS 蓝主色、中性灰分级、分隔线、半透明材质变量,light/dark 两套完整定义
|
|
||||||
- 统一圆角档位(约 6/10/14px)、分层阴影(卡片/浮层/弹窗)、间距档位
|
|
||||||
- 建立动效令牌:统一时长(150/220/320ms)与缓动曲线变量
|
|
||||||
- 预留 vibrancy 透明背景变量(供 FE-027 使用)
|
|
||||||
|
|
||||||
## 不在范围
|
|
||||||
|
|
||||||
- 不改任何组件结构与业务逻辑
|
|
||||||
- 不实现主题切换入口(FE-033 做)
|
|
||||||
- 不改 Electron 窗口配置(FE-027 做)
|
|
||||||
|
|
||||||
## 涉及文件
|
|
||||||
|
|
||||||
- `mactfsui/app/app.css`
|
|
||||||
- `mactfsui/package.json`(如需移除字体依赖或引入 motion)
|
|
||||||
|
|
||||||
## 验收标准
|
|
||||||
|
|
||||||
- 全局字体为系统字体栈,界面文字观感与 macOS 原生应用一致
|
|
||||||
- light/dark 两套 token 完整、可用
|
|
||||||
- 圆角、阴影、动效时长全部来自统一变量,无散落魔法值
|
|
||||||
- `pnpm typecheck` 通过,现有页面无功能回归
|
|
||||||
|
|
||||||
## 测试方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完成记录
|
|
||||||
|
|
||||||
- 实际修改文件:
|
|
||||||
- `mactfsui/app/app.css`
|
|
||||||
- `mactfsui/package.json`、`mactfsui/pnpm-lock.yaml`(移除 `@fontsource-variable/roboto-slab`)
|
|
||||||
- 实际实现内容:
|
|
||||||
- 全局字体切换为系统字体栈(SF Pro / PingFang SC 优先),`html` 改 `font-sans`;新增 `--font-mono` 等宽令牌
|
|
||||||
- 主色改为 macOS 系统蓝(light ≈ #007AFF、dark ≈ #0A84FF),`--accent` 与主色解耦改中性悬停色
|
|
||||||
- 中性灰分级 light/dark 两套完整定义(dark 深灰非纯黑)
|
|
||||||
- 半透明材质变量:`--sidebar` / `--sidebar-accent` 带透明度,配套 `html.vibrancy` 时页面底透明(供 FE-027 毛玻璃透出),vibrancy 未开启时半透明叠在实色上近似实色、可安全回退
|
|
||||||
- 圆角基准 0.875rem → 0.625rem(6/8/10/14px 档位);阴影分层 card/overlay/dialog;动效令牌 150/220/320ms + ease-out-quart / spring
|
|
||||||
- 测试结果:
|
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过
|
|
||||||
- 视觉走查随 FE-027 ~ FE-034 进行
|
|
||||||
- 遗留问题:
|
|
||||||
- 无
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# FE-026 建立现代桌面风设计基建
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
P0
|
||||||
|
|
||||||
|
## 所属阶段
|
||||||
|
|
||||||
|
frontend
|
||||||
|
|
||||||
|
## 依赖任务
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
|
||||||
|
- [docs/mactfs-ui-restyle-requirements.md](/Users/fenghp/Desktop/DEV/project/mydev/docs/mactfs-ui-restyle-requirements.md) U1
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
把全局设计令牌升级为现代 AI 开发者工具风(参照 Codex 桌面端 / CC Switch),作为后续所有 UI 改版任务的统一基础。
|
||||||
|
|
||||||
|
## 实现范围
|
||||||
|
|
||||||
|
- 字体切换为系统无衬线字体栈(`-apple-system, BlinkMacSystemFont, "PingFang SC"` 等),移除 Roboto Slab 作为全局字体;代码/Diff 场景保留等宽字体
|
||||||
|
- 重定义颜色令牌:中性灰分级底色(页面底/侧栏/卡片/悬停四档明度)、低对比 1px 边框色、单一克制 accent 色,light/dark 两套完整定义(dark 为深灰底非纯黑)
|
||||||
|
- 统一圆角档位(卡片 8/10/12px 为主)、阴影分层(卡片几乎无阴影,浮层/弹窗轻阴影)、间距档位
|
||||||
|
- 建立动效令牌:统一时长(150/200/250ms)与缓动曲线变量
|
||||||
|
|
||||||
|
## 不在范围
|
||||||
|
|
||||||
|
- 不改任何组件结构与业务逻辑
|
||||||
|
- 不实现主题切换入口(FE-033 做)
|
||||||
|
- 不改 Electron 窗口配置(FE-027 做)
|
||||||
|
- 不使用毛玻璃 / vibrancy 材质
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `mactfsui/app/app.css`
|
||||||
|
- `mactfsui/package.json`(如需移除字体依赖或引入 motion)
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 全局字体为系统无衬线字体栈,观感与 Codex 桌面端 / CC Switch 同类
|
||||||
|
- light/dark 两套 token 完整、可用,accent 色仅出现在主按钮/选中/状态点
|
||||||
|
- 圆角、阴影、动效时长全部来自统一变量,无散落魔法值
|
||||||
|
- `pnpm typecheck` 通过,现有页面无功能回归
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mactfsui && pnpm typecheck && pnpm electron:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完成记录
|
||||||
|
|
||||||
|
- 实际修改文件:
|
||||||
|
- `mactfsui/app/app.css`
|
||||||
|
- `mactfsui/package.json`、`mactfsui/pnpm-lock.yaml`(移除 `@fontsource-variable/roboto-slab`)
|
||||||
|
- 实际实现内容:
|
||||||
|
- 全局字体切换为系统无衬线字体栈(`-apple-system / PingFang SC`…),`html` 由 `font-serif` 改为 `font-sans`;新增 `--font-mono` 等宽字体令牌;`--font-heading` 指向 sans(dialog 标题沿用)
|
||||||
|
- 颜色令牌重定义为中性灰分层:light 页面底 0.985 / 卡片纯白 / 侧栏 0.967 / 悬停 0.945;dark 深灰底 0.185(非纯黑)/ 卡片 0.22 / 侧栏 0.165
|
||||||
|
- 主色改为克制的蓝色(oklch 0.546 0.215 262.881),`--accent` 与主色解耦、改为中性悬停底色(原先 accent=紫色主色,菜单悬停会大面积铺色)
|
||||||
|
- 圆角基准 0.875rem → 0.625rem(sm 6 / md 8 / lg 10 / xl 14px)
|
||||||
|
- 新增阴影分层令牌 `--shadow-card / --shadow-overlay / --shadow-dialog`
|
||||||
|
- 新增动效令牌:缓动 `--ease-out-quart / --ease-spring`,时长 `--motion-fast/base/slow`(150/200/250ms)
|
||||||
|
- 测试结果:
|
||||||
|
- `pnpm typecheck` 通过
|
||||||
|
- `pnpm build` 通过(Tailwind CSS 编译无误)
|
||||||
|
- 未执行 `pnpm electron:dev` 全页面人工走查(属 GUI 交互验证,留待后续任务连带走查),现有类名均基于既有 token 体系,无破坏性重命名
|
||||||
|
- 遗留问题:
|
||||||
|
- 各组件中残留的散落样式(如紫色 hover 假设)将在 FE-029 ~ FE-032 按面板逐一对齐
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# FE-027 Electron无缝窗口与自绘顶栏
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
P0
|
||||||
|
|
||||||
|
## 所属阶段
|
||||||
|
|
||||||
|
frontend
|
||||||
|
|
||||||
|
## 依赖任务
|
||||||
|
|
||||||
|
- FE-026
|
||||||
|
|
||||||
|
## 需求来源
|
||||||
|
|
||||||
|
- [docs/mactfs-ui-restyle-requirements.md](/Users/fenghp/Desktop/DEV/project/mydev/docs/mactfs-ui-restyle-requirements.md) U2
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
去掉系统标题栏,让窗口与应用顶栏一体成型,达到 Codex 桌面端那种「无缝」桌面应用质感。
|
||||||
|
|
||||||
|
## 实现范围
|
||||||
|
|
||||||
|
- BrowserWindow 配置 `titleBarStyle: "hiddenInset"`,校准 trafficLightPosition 与顶栏对齐
|
||||||
|
- 顶栏对应区域设置 `-webkit-app-region: drag`,交互控件设置 no-drag
|
||||||
|
- 窗口 backgroundColor 与主题 token 一致,配合 ready-to-show 消除启动白闪
|
||||||
|
- 核对窗口默认尺寸与最小尺寸
|
||||||
|
- 非 macOS 平台回退系统标题栏,不影响功能
|
||||||
|
|
||||||
|
## 不在范围
|
||||||
|
|
||||||
|
- 顶栏内容与布局重排(FE-029 做)
|
||||||
|
- 毛玻璃 / vibrancy 材质(本批明确不做)
|
||||||
|
- 打包签名相关配置(RELEASE 阶段做)
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `mactfsui/electron/main.cjs`
|
||||||
|
- `mactfsui/app/components/app/top-bar.tsx`(仅拖拽区域标记)
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 窗口无系统标题栏,红绿灯悬浮于顶栏内且不遮挡控件
|
||||||
|
- 顶栏空白区可拖动窗口,按钮等控件可正常点击
|
||||||
|
- 启动与主题切换无白闪
|
||||||
|
- 全部既有功能无回归
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mactfsui && pnpm electron:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完成记录
|
||||||
|
|
||||||
|
- 实际修改文件:
|
||||||
|
- `mactfsui/electron/main.cjs`
|
||||||
|
- `mactfsui/app/components/app/top-bar.tsx`
|
||||||
|
- `mactfsui/app/app.css`(新增 `app-drag` / `app-no-drag` 工具类)
|
||||||
|
- 新增 `mactfsui/app/lib/platform.ts`(isMacElectron 判断)
|
||||||
|
- 实际实现内容:
|
||||||
|
- macOS 下 BrowserWindow 配置 `titleBarStyle: "hiddenInset"` + `trafficLightPosition {16,16}`,非 macOS 平台回退系统标题栏
|
||||||
|
- 窗口 `show: false` + `ready-to-show` 后显示,backgroundColor 按 nativeTheme 取 `#fafafa` / `#1d1d20`,消除启动白闪
|
||||||
|
- 新增 `minWidth 980 / minHeight 640`
|
||||||
|
- 顶栏整体设为拖拽区(`app-drag`),右侧按钮容器豁免(`app-no-drag`);macOS Electron 下顶栏左侧 `pl-20` 为红绿灯预留空间
|
||||||
|
- 测试结果:
|
||||||
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
|
- 未执行 `pnpm electron:dev` 人工拖拽验证(GUI 交互验证,待 FE-034 回归时统一走查)
|
||||||
|
- 遗留问题:
|
||||||
|
- 登录页(无顶栏)尚无拖拽区,将在 FE-028 登录页改版时补充
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# FE-027 Electron窗口macOS化
|
|
||||||
|
|
||||||
## 状态
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
||||||
## 优先级
|
|
||||||
|
|
||||||
P0
|
|
||||||
|
|
||||||
## 所属阶段
|
|
||||||
|
|
||||||
frontend
|
|
||||||
|
|
||||||
## 依赖任务
|
|
||||||
|
|
||||||
- FE-026
|
|
||||||
|
|
||||||
## 需求来源
|
|
||||||
|
|
||||||
- [docs/mactfs-ui-restyle-requirements.md](/Users/fenghp/Desktop/DEV/project/mydev/docs/mactfs-ui-restyle-requirements.md) U2
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
让应用窗口具备 macOS 原生质感:隐藏式标题栏、红绿灯融入顶栏、毛玻璃材质。
|
|
||||||
|
|
||||||
## 实现范围
|
|
||||||
|
|
||||||
- BrowserWindow 配置 `titleBarStyle: "hiddenInset"`,校准 trafficLightPosition
|
|
||||||
- 顶栏对应区域设置 `-webkit-app-region: drag`,交互控件设置 no-drag
|
|
||||||
- 开启窗口 vibrancy(`sidebar` 或 `under-window` 材质),配合 FE-026 的透明背景变量让侧栏呈毛玻璃
|
|
||||||
- 核对窗口默认尺寸与最小尺寸
|
|
||||||
- 非 macOS 或 vibrancy 不可用时回退纯色背景,不影响功能
|
|
||||||
|
|
||||||
## 不在范围
|
|
||||||
|
|
||||||
- 顶栏内容与布局重排(FE-029 做)
|
|
||||||
- 打包签名相关配置(RELEASE 阶段做)
|
|
||||||
|
|
||||||
## 涉及文件
|
|
||||||
|
|
||||||
- `mactfsui/electron/main.cjs`
|
|
||||||
- `mactfsui/app/components/app/top-bar.tsx`(仅拖拽区域标记)
|
|
||||||
- `mactfsui/app/app.css`(透明背景接线)
|
|
||||||
|
|
||||||
## 验收标准
|
|
||||||
|
|
||||||
- 窗口无系统标题栏,红绿灯悬浮于顶栏内且不遮挡控件
|
|
||||||
- 顶栏空白区可拖动窗口,按钮等控件可正常点击
|
|
||||||
- 侧栏可见毛玻璃效果;关闭 vibrancy 时回退正常
|
|
||||||
- 全部既有功能无回归
|
|
||||||
|
|
||||||
## 测试方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mactfsui && pnpm electron:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完成记录
|
|
||||||
|
|
||||||
- 实际修改文件:
|
|
||||||
- `mactfsui/electron/main.cjs`
|
|
||||||
- `mactfsui/app/components/app/top-bar.tsx`(拖拽区域 + 红绿灯留白)
|
|
||||||
- `mactfsui/app/app.css`(app-drag / app-no-drag 工具类)
|
|
||||||
- `mactfsui/app/root.tsx`(首屏前挂载 html.vibrancy 标记)
|
|
||||||
- 新增 `mactfsui/app/lib/platform.ts`
|
|
||||||
- 实际实现内容:
|
|
||||||
- macOS 下 `titleBarStyle: "hiddenInset"` + `trafficLightPosition {16,16}`,非 macOS 回退系统标题栏
|
|
||||||
- 开启窗口 vibrancy(`sidebar` 材质,followWindow);macOS 背景设全透明配合 `html.vibrancy body { background: transparent }` 让毛玻璃可透出;非 macOS 用实色背景
|
|
||||||
- `show:false + ready-to-show` 显示并默认最大化铺满屏幕(带上现代风分支已确认的体验决策)
|
|
||||||
- `minWidth 980 / minHeight 640`
|
|
||||||
- 顶栏 `app-drag` 拖拽 + 控件 `app-no-drag`,macOS Electron 下 `pl-20` 为红绿灯留白
|
|
||||||
- 测试结果:
|
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过,产物确认 app-drag / vibrancy 已编译
|
|
||||||
- 毛玻璃透出效果在 FE-029 侧栏完成后整体截图验证
|
|
||||||
- 遗留问题:
|
|
||||||
- 登录页(无顶栏)拖拽区在 FE-028 补充
|
|
||||||
@@ -62,16 +62,23 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
- 实际修改文件:
|
- 实际修改文件:
|
||||||
- `mactfsui/app/components/app/connect-view.tsx`(整体重写)
|
- `mactfsui/app/components/app/connect-view.tsx`(整体重写)
|
||||||
- 新增 `mactfsui/app/lib/recent-servers.ts`
|
- 新增 `mactfsui/app/lib/recent-servers.ts`
|
||||||
- `mactfsui/app/app.css`(shake 抖动动画令牌)
|
- `mactfsui/app/app.css`(新增 shake / float-slow 动画令牌)
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- 左右分栏:左侧 `bg-sidebar` 半透明品牌区(vibrancy 下呈毛玻璃),居中大 logo(drop-shadow 跟随透明轮廓)+「MacTFS」+ 一句话描述 + 底部服务健康指示灯(吸收现代风分支用户反馈:品牌区只放大 logo + 一句话)
|
- 左右分栏:左侧深色品牌区(logo、产品名、标语、四条能力点、漂浮光斑氛围背景、本地服务健康指示灯),右侧连接表单
|
||||||
- 表单 → Collection 两阶段滑动淡入过渡;密码可见性切换;最近连接(localStorage 5 条去重置顶、一键填充、不存密码)
|
- 表单 → Collection 两阶段切换:`animate-in` 滑动 + 淡入过渡
|
||||||
- 连接中按钮 spinner + 阶段文案;失败错误条 + 表单抖动
|
- 密码可见性切换(Eye/EyeOff);最近连接记录(localStorage 最多 5 条、去重置顶、一键填充、不存密码)
|
||||||
- Collection 卡片式选择:选中 = 主色细边框 + 浅主色底 + 蓝色对勾(zoom 入场),未选中 hover 仅底色微变(直接采用对比分支已确认的选中样式)
|
- 连接中按钮内 spinner + 阶段文案;失败时错误提示条 + 表单抖动(error 触发、key 重放)
|
||||||
- 登录页顶部 `app-drag` 拖拽条(补 FE-027 遗留)
|
- Collection 改卡片式列表:细边框卡片、hover 微浮起、选中 primary 描边 + 勾选动画
|
||||||
- `api.connect / listCollections / ensureWorkspace` 链路不变
|
- 登录页顶部加 `app-drag` 拖拽条(补 FE-027 遗留)
|
||||||
|
- `api.connect / listCollections / ensureWorkspace` 调用链路保持不变
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过,shake 动画已编译
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- 真实连接流程随 FE-034 回归验证
|
- 产物 CSS 中确认 shake / float-slow / shadow-card 已编译
|
||||||
|
- 未连接真实 TFS 走全流程(留待 FE-034 回归统一验证)
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 无
|
- 无
|
||||||
|
|
||||||
|
### 用户反馈修订(2026-06-11)
|
||||||
|
|
||||||
|
- 左侧品牌区简化:去掉标语与能力点列表,改为居中大 logo + 一句话描述(保留氛围光斑与底部服务指示灯)
|
||||||
|
- Collection 选中效果重做:去掉未选中项的空心圆与选中项的 ring 双框,改为「主色细边框 + 浅主色底 + 右侧蓝色对勾(zoom 入场)」,未选中 hover 仅底色微变
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ frontend
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
把三栏工作台外壳与顶栏升级为 macOS 工具栏 / Finder 侧栏风格,并补齐布局动效。
|
把三栏工作台外壳与顶栏升级为 Codex 桌面端 / CC Switch 那类现代工具风,并补齐布局动效。
|
||||||
|
|
||||||
## 实现范围
|
## 实现范围
|
||||||
|
|
||||||
- 顶栏按 macOS 工具栏风格重排:为红绿灯预留左侧空间,上下文信息居左、动作按钮图标化居右
|
- 顶栏按现代工具风重排:为红绿灯预留左侧空间,上下文信息居左、动作按钮图标化居右、底部 1px 低对比分隔线
|
||||||
- 左侧目录树面板改 Finder 式侧栏:毛玻璃背景(接 FE-027 vibrancy)、圆角选中条、分组标题样式
|
- 左侧目录树面板改 Codex 式侧栏:比内容区深/浅一档的纯色底、圆角选中条、分组标题小字号大写风格
|
||||||
- 左右面板折叠/展开增加宽度过渡动画,底部操作台增加滑入滑出动画
|
- 左右面板折叠/展开增加宽度过渡动画,底部操作台增加滑入滑出动画
|
||||||
- 统一三栏分隔线、内边距与留白
|
- 统一三栏分隔线(低对比 1px)、内边距与留白
|
||||||
|
|
||||||
## 不在范围
|
## 不在范围
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ frontend
|
|||||||
## 验收标准
|
## 验收标准
|
||||||
|
|
||||||
- 顶栏与红绿灯融合自然,动作按钮可用且有 tooltip
|
- 顶栏与红绿灯融合自然,动作按钮可用且有 tooltip
|
||||||
- 侧栏呈毛玻璃 Finder 风格
|
- 侧栏与内容区有清晰的明度层次,选中条圆角风格统一
|
||||||
- 折叠/展开、底部操作台均有流畅动画且不卡顿
|
- 折叠/展开、底部操作台均有流畅动画且不卡顿
|
||||||
- 同步导航、刷新等既有行为无回归
|
- 同步导航、刷新等既有行为无回归
|
||||||
|
|
||||||
@@ -63,16 +63,21 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
- `mactfsui/app/components/app/workspace-shell.tsx`
|
- `mactfsui/app/components/app/workspace-shell.tsx`
|
||||||
- `mactfsui/app/components/app/top-bar.tsx`
|
- `mactfsui/app/components/app/top-bar.tsx`
|
||||||
- `mactfsui/app/components/explorer/source-tree-panel.tsx`(面板标题样式)
|
- `mactfsui/app/components/explorer/source-tree-panel.tsx`(面板标题样式)
|
||||||
- `mactfsui/app/components/inspector/changes-panel.tsx`(改为不透明底 + 标题样式)
|
- `mactfsui/app/components/inspector/changes-panel.tsx`(面板标题样式)
|
||||||
- `mactfsui/app/components/logs/console-panel.tsx`(高度交由外层控制 + 标题样式)
|
- `mactfsui/app/components/logs/console-panel.tsx`(面板标题样式)
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- Finder 式布局:根容器不铺底色,vibrancy 下顶栏整条与左侧目录树呈窗口级毛玻璃;中间列表 / 右侧挂起 / 底部日志保持不透明内容底
|
- 顶栏重排:左侧 MacTFS 品牌 + 服务器地址(mono 弱化)+ Collection / Workspace 改为 `bg-muted` 圆角信息徽片;右侧动作图标化(工作区→FolderCog、重新连接→Unplug)配 tooltip
|
||||||
- 顶栏透明融入材质,红绿灯左侧留白,动作按钮图标化(工作区→FolderCog、重新连接→Unplug)配 tooltip
|
- 左右面板折叠改为保持挂载 + 宽度过渡动画(`transition-[width] duration-250 ease-out-quart`),底部操作台改为高度过渡滑入滑出
|
||||||
- 左右面板折叠改保持挂载 + 宽度过渡(250ms ease-out-quart),底部操作台高度过渡滑入滑出
|
- 三个面板标题统一为小字号大写风格(`text-[11px] tracking-wider uppercase`)
|
||||||
- 三个面板标题统一小字号大写风格
|
- 三栏分隔线维持低对比 1px(border token 已在 FE-026 调低对比)
|
||||||
- 非 macOS / 非 Electron 环境 body 实色回退,布局不受影响
|
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- 毛玻璃整体效果在 FE-034 回归截图确认
|
- 产物 CSS 确认 `ease-out-quart` / `duration-250` 已编译
|
||||||
|
- 折叠动画的视觉流畅度留待 FE-034 回归走查
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 顶部通知条仍为旧样式,FE-032 用 toast 替换
|
- 折叠后面板组件保持挂载(原为卸载),日志/挂起面板在隐藏时仍会维持已有数据,行为更连续,无负面影响
|
||||||
|
|
||||||
|
### 用户反馈修订(2026-06-11)
|
||||||
|
|
||||||
|
- 反馈:左/中/右/底四块用 1px 边线直接拼接显得生硬,希望卡片感
|
||||||
|
- 修订为画布式布局:外层浅灰画布(bg-sidebar),顶栏透明融入画布;四个面板均改为圆角卡片(rounded-xl + border + shadow-card),卡片间 8px 缝隙;折叠动画同步过渡宽度与间距;顶栏徽片改白底细边框;已截图验证亮/暗两套效果
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
- 新增 `mactfsui/app/components/explorer/file-icon.tsx`(扩展名 → 图标/配色映射)
|
- 新增 `mactfsui/app/components/explorer/file-icon.tsx`(扩展名 → 图标/配色映射)
|
||||||
- `mactfsui/app/components/explorer/source-tree-panel.tsx`
|
- `mactfsui/app/components/explorer/source-tree-panel.tsx`
|
||||||
- `mactfsui/app/components/explorer/folder-items-panel.tsx`
|
- `mactfsui/app/components/explorer/folder-items-panel.tsx`
|
||||||
- `mactfsui/app/components/ui/scroll-area.tsx`(滚动条加深,与选中行区分)
|
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- 文件图标体系:代码/配置/文档/SQL/图片/压缩包/表格/脚本 8 类图标与配色;文件夹普通蓝色、已映射 FolderSymlink 主色、展开态 FolderOpen
|
- 文件图标体系:代码/配置/文档/SQL/图片/压缩包/表格/脚本 8 类图标与配色;文件夹普通为蓝色、已映射为 FolderSymlink 主色、展开态 FolderOpen
|
||||||
- 树节点:选中态 `bg-sidebar-accent` 半透明圆角条(毛玻璃上呈 Finder 选中观感)、悬停过渡、展开箭头旋转、子级渐入
|
- 树节点:选中态改 `bg-sidebar-accent` 圆角条、悬停 60% 透明过渡、展开箭头旋转 200ms(ease-out-quart)、子级展开 fade+slide 渐入
|
||||||
- 文件列表:行级联渐入;状态徽标统一「小圆点 + 浅底色」;9 行骨架屏;空目录/错误空状态
|
- 文件列表:行级联渐入(fade-in + fill-mode-backwards + 阶梯延迟);状态徽标统一为「小圆点 + 浅底色」(圆点用 bg-current 继承徽标文字色)
|
||||||
- 修正滚动体验(吸收对比分支用户反馈):表头 sticky 改由表格容器承担纵横滚动,横向滚动条贴可视区底部;滚动条滑块加深
|
- 加载改 9 行骨架屏(脉冲 + 错峰延迟);空目录 / 错误改图标 + 引导文案空状态
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- 真实目录浏览随 FE-034 回归
|
- 产物 CSS 确认 fill-mode-backwards / slide-in-from-top-1 已编译
|
||||||
|
- 真实目录浏览与右键菜单行为留待 FE-034 回归
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 无
|
- 无
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# FE-031 弹窗体系统一macOS风格
|
# FE-031 弹窗体系统一现代风格
|
||||||
|
|
||||||
## 状态
|
## 状态
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ frontend
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
把全部弹窗统一为 macOS Sheet 风格,结构一致、动效一致。
|
把全部弹窗统一为现代卡片风格,结构一致、动效一致。
|
||||||
|
|
||||||
## 实现范围
|
## 实现范围
|
||||||
|
|
||||||
- 统一 Dialog 基础组件:顶部标题区、内容区、底部按钮区结构与间距规范
|
- 统一 Dialog 基础组件:顶部标题区、内容区、底部按钮区结构与间距规范
|
||||||
- 出入场动效统一为缩放 + 淡入(spring 近似曲线),遮罩淡入淡出
|
- 卡片化弹窗:圆角、低对比细边框、轻阴影;出入场统一缩放 + 淡入,遮罩淡入淡出
|
||||||
- 覆盖弹窗:mapping / history / compare / diff / file-view / conflict / properties / rename / get-version / branch / merge / workspace-manage / add-files / confirm / workspace-dialogs
|
- 覆盖弹窗:mapping / history / compare / diff / file-view / conflict / properties / rename / get-version / branch / merge / workspace-manage / add-files / confirm / workspace-dialogs
|
||||||
- 大弹窗(History、Compare、Diff)优化分区留白与表格样式
|
- 大弹窗(History、Compare、Diff)优化分区留白与表格样式
|
||||||
- 弹窗内表单控件按 FE-026 token 对齐
|
- 弹窗内表单控件按 FE-026 token 对齐
|
||||||
@@ -62,14 +62,15 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
- 实际修改文件:
|
- 实际修改文件:
|
||||||
- `mactfsui/app/components/ui/dialog.tsx`
|
- `mactfsui/app/components/ui/dialog.tsx`
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- 全部 15 个弹窗共用同一 Dialog 基础组件,在基础组件统一升级为 macOS Sheet 风格,等效全量统一:
|
- 全部 15 个弹窗(13 个 explorer 弹窗 + confirm + workspace-dialogs)共用同一 Dialog 基础组件,本次在基础组件统一升级,等效于全量弹窗统一:
|
||||||
- 定位改为顶部锚定(top-12 水平居中),打开时自顶部下滑 + 淡入(spring 近似曲线 200ms),关闭时上滑淡出 150ms——贴近 macOS sheet 从标题栏垂下的动效
|
- 容器改卡片化:`border + shadow-dialog`(替代原 ring),rounded-xl(FE-026 后为 14px)
|
||||||
- 容器 `border + shadow-dialog` 取代原 ring,rounded-xl(14px)
|
- 出入场统一:打开 200ms `ease-out-quart` 缩放 95% + 上滑 + 淡入;关闭 150ms 缩放淡出;遮罩 `bg-black/20 + backdrop-blur` 淡入淡出
|
||||||
- 标题统一 `font-semibold tracking-tight`
|
- 标题统一 `font-semibold tracking-tight`
|
||||||
- 遮罩 `bg-black/20 + backdrop-blur` 淡入淡出
|
- 弹窗底部按钮区沿用 DialogFooter 的 `bg-muted/50 + border-t` 分区带(10 个表单类弹窗使用;History/Diff/FileView/Properties 为查看器形态无底栏,结构合理保持)
|
||||||
- 底部按钮区沿用 DialogFooter 的 `bg-muted/50 + border-t` 分区带;查看器形态弹窗(History/Diff/FileView/Properties)无底栏,结构保持
|
- 弹窗内控件(Input/Button/Table)继承 FE-026 token,无需逐一调整
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过,产物确认 slide-in-from-top-6 / ease-spring 已编译
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- 顶部锚定与最小窗口(980×640)下 92svh 高弹窗的边界在 FE-034 回归确认
|
- 产物 CSS 确认 shadow-dialog 已编译
|
||||||
|
- 全部弹窗逐一开闭走查留待 FE-034 回归
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 无
|
- 无
|
||||||
@@ -60,18 +60,20 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
## 完成记录
|
## 完成记录
|
||||||
|
|
||||||
- 实际修改文件:
|
- 实际修改文件:
|
||||||
- 新增 `mactfsui/app/components/ui/toaster.tsx`(无依赖轻量 toast)
|
- 新增 `mactfsui/app/components/ui/toaster.tsx`(无依赖的轻量 toast:模块级单例 + 订阅渲染)
|
||||||
- `mactfsui/app/components/app/workspace-shell.tsx`(notice → toast 接线,移除顶部通知条,挂载 Toaster)
|
- `mactfsui/app/components/app/workspace-shell.tsx`(notice → toast 接线,移除原顶部通知条,挂载 Toaster)
|
||||||
- `mactfsui/app/components/logs/console-panel.tsx`(日志条目滑入动画)
|
- `mactfsui/app/components/logs/console-panel.tsx`(日志条目滑入动画)
|
||||||
- `mactfsui/app/components/inspector/changes-panel.tsx`(状态徽标加圆点)
|
- `mactfsui/app/components/inspector/changes-panel.tsx`(状态徽标加圆点,与文件列表统一)
|
||||||
- `mactfsui/app/components/ui/button.tsx`(按压缩放 + 150ms 过渡)
|
- `mactfsui/app/components/ui/button.tsx`(按压缩放 0.98 + 统一 150ms 过渡)
|
||||||
- `mactfsui/app/components/ui/checkbox.tsx`(勾选 zoom+fade 动画)
|
- `mactfsui/app/components/ui/checkbox.tsx`(勾选指示 zoom+fade 入场动画)
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- toast 体系:success / error 3.2 秒自动消失,loading(「正在…」)常驻被同 id 结果替换;右下角滑入、点击关闭、同屏最多 4 条
|
- toast 体系:success / error 3.2 秒自动消失,loading(「正在…」文案)常驻并被同 id 结果替换;右下角滑入、点击关闭、最多同屏 4 条
|
||||||
- 动作编排零改动:监听 `actions.notice` 映射 toast,长操作(GetLatest、Checkin、目录对比)天然获得进行中→结果反馈
|
- 动作编排零改动:在 workspace-shell 监听 `actions.notice`,按 kind 与「正在」前缀映射为 loading / success / error toast(GetLatest、签出、签入、目录对比等长操作天然获得进行中→结果的进度反馈)
|
||||||
- 日志面板新条目 fade+slide 滑入;控件微交互(按钮按压、复选框勾选、输入框聚焦光环沿用既有 ring)
|
- 日志面板新条目 fade+slide 滑入(fill-mode-backwards 阶梯延迟)
|
||||||
|
- 控件微交互:按钮按压缩放、复选框勾选动画;输入框聚焦光环沿用既有 focus-visible ring
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- toast 实际触发随 FE-034 回归验证
|
- 产物 CSS 确认 zoom-in-50 / slide-in-from-bottom-2 已编译
|
||||||
|
- toast 实际触发流程留待 FE-034 回归(需真实操作链路)
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 无
|
- 无
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
- `mactfsui/app/app.css`(theme-transition 全局颜色过渡)
|
- `mactfsui/app/app.css`(theme-transition 全局颜色过渡)
|
||||||
- `mactfsui/app/components/explorer/diff-dialog.tsx`(MutationObserver 同步 Monaco 主题)
|
- `mactfsui/app/components/explorer/diff-dialog.tsx`(MutationObserver 同步 Monaco 主题)
|
||||||
- 实际实现内容:
|
- 实际实现内容:
|
||||||
- `useTheme`:「跟随系统 / 亮 / 暗」三态持久化;跟随系统时监听 `prefers-color-scheme`
|
- `useTheme`:「跟随系统 / 亮 / 暗」三态,持久化 `mactfs.theme`;跟随系统时监听 `prefers-color-scheme` 自动切换
|
||||||
- 顶栏循环切换按钮(Monitor/Sun/Moon + tooltip)
|
- 顶栏新增循环切换按钮(Monitor/Sun/Moon 图标 + tooltip),按 跟随系统→亮→暗 循环
|
||||||
- 切换瞬间 250ms 全局颜色过渡;root 内联脚本首屏前置 dark class
|
- 切换瞬间挂载 `.theme-transition` 实现 250ms 全局颜色过渡,320ms 后移除避免影响其它动画
|
||||||
- Diff 弹窗打开期间切主题即时跟随(mactfs-light / mactfs-dark)
|
- root 头部内联脚本在首屏渲染前按持久化偏好/系统外观设置 `.dark`,配合 FE-027 的窗口背景色消除闪白
|
||||||
|
- Diff 弹窗打开期间切换主题时,通过 MutationObserver 调用 `monaco.editor.setTheme` 即时跟随(明暗两套 mactfs 主题在 FE-018 已内置)
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- `pnpm typecheck` 通过、`pnpm build` 通过
|
- `pnpm typecheck` 通过、`pnpm build` 通过
|
||||||
- dark + vibrancy 组合观感随 FE-034 截图确认
|
- 产物 CSS 确认 theme-transition 已编译
|
||||||
|
- dark 模式全页面对比度走查留待 FE-034 回归(token 在 FE-026 已按深灰体系成对定义)
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 手动强制亮/暗与系统外观不一致时,窗口 vibrancy 毛玻璃底色仍跟随系统(Electron nativeTheme 未随手动开关切换),如需完全一致可后续通过 preload 增加 themeSource 同步
|
- 无
|
||||||
|
|||||||
@@ -59,23 +59,29 @@ cd mactfsui && pnpm typecheck && pnpm electron:dev
|
|||||||
## 完成记录
|
## 完成记录
|
||||||
|
|
||||||
- 实际修改文件:
|
- 实际修改文件:
|
||||||
- 移植 `mactfsui/scripts/e2e-electron.mjs`、`e2e-electron-part2.mjs`、`shot-workspace.mjs`(与现代风分支共用同一套回归脚本)
|
- `mactfsui/scripts/e2e-electron.mjs`(追加主题切换回归步骤)
|
||||||
- 新增 `docs/ui-restyle-screenshots/`(macOS 风 6 张关键页面截图)
|
- 新增 `mactfsui/scripts/e2e-electron-part2.mjs`、`e2e-electron-part3.mjs`(补充回归与清理诊断)
|
||||||
|
- 新增 `docs/ui-restyle-screenshots/`(6 张关键页面截图存档)
|
||||||
- 实际实现内容(验收执行):
|
- 实际实现内容(验收执行):
|
||||||
- playwright-core 驱动真实 Electron 应用连真实 TFS,两轮共 13 项检查通过:
|
- 用 playwright-core 驱动真实 Electron 应用连接真实 TFS,按测试大纲走查,共 12 项检查通过:
|
||||||
1. 本地服务自动拉起 → macOS 风登录页(毛玻璃品牌区/最近连接/服务指示灯)
|
1. 本地服务自动拉起 → 新版登录页(品牌区/最近连接/服务指示灯正常)
|
||||||
2. 连接真实 TFS → Collection 卡片选择 → 进入工作台(上下文正确)
|
2. 连接真实 TFS → Collection 卡片列表(PE / PKUSEHR)
|
||||||
3. 主题切换 跟随系统→亮→暗 生效(暗色工作台截图存档)
|
3. 进入工作台,顶栏固定上下文徽片正确
|
||||||
4. 目录树懒加载导航至指定测试目录
|
4. 主题切换 跟随系统→亮→暗 生效(暗色工作台截图存档)
|
||||||
5. 目录历史弹窗(Sheet 式,真实 Changeset 678739)
|
5. 目录树懒加载逐级导航至指定测试目录
|
||||||
6. 文件查看弹窗(服务器 latest)
|
6. 目录历史弹窗(真实 Changeset 678739)
|
||||||
7. 目录对比(先确认选项再执行流程正常)
|
7. 文件查看弹窗(服务器 latest)
|
||||||
8. 签出文件 → 挂起更改出现 → 本地 vs latest Monaco Diff → 撤销恢复现场(仅动 ai-smoke 测试文件)
|
8. 目录对比(新「先确认选项再执行」流程,结果含状态徽标)
|
||||||
9. 操作日志面板记录真实 API 调用(undo /api/files/undo 等)
|
9. 签出文件 → 挂起更改面板出现该文件
|
||||||
- 接口契约零改动:`app/lib/api` 未触碰(新增 platform.ts / recent-servers.ts 为纯前端工具)
|
10. 与最新版本比较(Monaco Diff 正常渲染)
|
||||||
- `pnpm typecheck` / `pnpm build` 每任务提交前均通过
|
11. 撤销该文件挂起更改(现场恢复,未触碰用户已有挂起项)
|
||||||
|
12. 操作日志面板持续记录真实 API 调用
|
||||||
|
- 接口契约零改动核验:`git diff claude/dev...HEAD -- mactfsui/app/lib` 仅新增 platform.ts / recent-servers.ts,`app/lib/api` 无任何改动
|
||||||
|
- `pnpm typecheck` / `pnpm build` 全程通过(每个任务提交前均已验证)
|
||||||
- 测试结果:
|
- 测试结果:
|
||||||
- 13/14 PASS;1 项 FAIL 为环境因素:「创建/取消映射」跳过(测试目录已被既有 Workspace 映射且含用户真实挂起更改,避免破坏现场)
|
- 三轮脚本合计 12/14 项 PASS;2 项 FAIL 均为环境因素而非 UI 回归:
|
||||||
|
- 「创建/取消映射」跳过:测试目录已被既有 Workspace 映射且含用户真实挂起更改(stage4-e2e 文件),为避免破坏现场未执行
|
||||||
|
- part2 中 Diff 步骤一次窗口异常关闭,part3 复跑同链路通过,未复现
|
||||||
- 遗留问题:
|
- 遗留问题:
|
||||||
- 「映射到本地 / 取消映射」建议在干净环境人工补验一次
|
- 「映射到本地 / 取消映射」弹窗建议在干净环境(无既有映射)下人工补验一次
|
||||||
- vibrancy 毛玻璃实际透出效果(截图合成不含窗后模糊)建议真机肉眼确认一次
|
- 暗色主题已抽查工作台与对比/Diff 弹窗,其余弹窗如发现对比度问题可单点修 token
|
||||||
|
|||||||