chore(v0.6): checkpoint foundation — Amber theme, P1 types, P3 config, design docs
Prior-session prep for the v0.6 Project Manager, checkpointed on the v0.6-projects branch as a clean base for the parallel builders: - P1 (src/types.ts): ProjectInfo + ProjectSessionRef contracts; Config gains projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck. - P3 (src/config.ts): parseBool + parseProjectRoots helpers; parse the 4 PROJECT_* env vars with defaults ([homeDir]/4/10000/true). - UI: public/style.css indigo -> Amber (#e3a64a) accent theme. - Docs: FEATURE_PROJECT_MANAGER.md (1:N session design), THEME.md, docs/mockups/ (final-amber.png is the locked visual). Typecheck (backend + web) green. No behavior change to runtime code yet; P2/P4/P5/P6 implement the feature on top of this.
This commit is contained in:
242
docs/FEATURE_PROJECT_MANAGER.md
Normal file
242
docs/FEATURE_PROJECT_MANAGER.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Feature: Project Manager 项目工作台 (v0.6)
|
||||
|
||||
> **状态**: 提案 / Draft(探索完成,待评审后进入 PLAN)
|
||||
> **目标**: 首页除了"会话选择器"外,新增一个 **项目面板**:展示主机上的所有项目(git 仓库),点一下某个项目 → 应用自动 spawn 一个 **Claude Code session**(cwd = 该仓库目录、自动运行 `claude`),并把 **tab 标签标注为 repo 名**。
|
||||
> **关键结论**: 终端机制几乎**零改动**——按目录 spawn (`attach.cwd`)、自动输入命令 (`initialInput`)、自定义标签 (`customTitle`) 全部已存在。新代码只有两块:**git 仓库发现 + `GET /projects` 端点** 和 **Projects 面板 UI**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 用户故事 (User Stories)
|
||||
|
||||
- **US1**(核心):作为开发者,我打开 web-terminal 首页,看到我所有项目的卡片墙;点 `web-terminal` 卡片,立刻开出一个新 tab,标签写着 `web-terminal`,里面已经 `cd` 到该目录并启动了 `claude`,我直接开始派活。
|
||||
- **US2**:每个项目卡片显示它的**当前分支**、是否有未提交改动 (dirty)、以及**是否已有正在运行的 session**(避免重复开)。
|
||||
- **US3**:如果某项目已经有一个跑着的 session,点卡片**直接进入那个 session**(而不是再开一个)。
|
||||
- **US4**:我能**搜索/过滤**项目(项目多时),把常用项目**置顶/收藏**。
|
||||
- **US5**:我能在卡片上选择"**新开** Claude session"还是"**只开 shell**(不自动跑 claude)",或"**resume** 该项目最近一次 Claude 会话"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 竞品参考 (Prior Art)
|
||||
|
||||
调研了主流的"项目选择器 → 启动 agent 会话"产品,提炼可借鉴点:
|
||||
|
||||
| 产品 | 项目发现方式 | 卡片展示的元数据 | 启动交互 | 可借鉴 UX |
|
||||
|---|---|---|---|---|
|
||||
| **CloudCLI / claudecodeui** (siteboon) | 自动扫描 `~/.claude/projects/` 配置目录 | 项目名、路径、历史 session 列表 | 点项目 → 列出其 sessions → 选一个 resume / 新建 | 侧边栏树形项目→会话二级结构;与本地 Claude 配置同步 |
|
||||
| **官方 Claude Code** | `~/.claude/projects/` 按 cwd 路径 keyed | cwd、历史会话 | `claude` 在 cwd 启动;`claude --resume <id>` | "项目 = 工作目录"这一极简心智模型 |
|
||||
| **Conductor** (Melty Labs, Mac) | 每个 workspace = 一个 **git worktree** | 看板:每个 agent 在干嘛 / 卡在哪 | 把 ticket 拉进 workspace 当任务描述 | 看板式 dashboard + "拉一个 ticket 进来就开干" |
|
||||
| **Crystal / Nimbalyst** (stravu) | 多个并行 session 跑在 git worktree | 并行会话、方案对比 | 一个项目开多个并行 session 比较产出 | 同一项目并行多 session、对比方案 |
|
||||
| **AgentsRoom** | 手机端远程 | 终端输出、待回答提示 | 手机监控 + 回答 prompt | 移动端"监控 + 审批"闭环(本项目 v0.3 已有类似) |
|
||||
| **tmuxinator / sesh** | 扫描配置/目录列出 project | 项目名 | 一键 attach "每项目一会话" | "每项目一会话"的命名与复用 |
|
||||
|
||||
**结论 / 我们要偷的点:**
|
||||
1. **项目 = 工作目录**(官方心智模型)——发现逻辑围绕"目录是不是 git 仓库"。
|
||||
2. **二级结构**(CloudCLI):项目 → 该项目下的历史/活动 session,点项目可"新建"或"进入已有"。
|
||||
3. **看板式状态**(Conductor):卡片直接显示该项目有没有 running session、在 working/waiting/idle 哪个状态(本项目 v0.3 hook 状态可复用)。
|
||||
4. **git 元数据**(分支 + dirty)让卡片"活"起来。
|
||||
5. 本项目的**差异化护城河**:自托管、不经云、多设备镜像同看、缩略图会话墙——Project 面板应与这些无缝融合(项目卡片可直接复用实时缩略图)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 功能需求 (Functional Requirements)
|
||||
|
||||
| ID | 需求 | 优先级 |
|
||||
|----|------|--------|
|
||||
| FR1 | 提供 `GET /projects`,返回主机上发现的项目列表(名称、绝对路径、是否 git、分支、dirty、最近活跃时间) | P0 |
|
||||
| FR2 | 项目发现:扫描 1~N 个**配置的根目录**(默认 `~`,可 env 覆盖)下的 git 仓库;并合并 `~/.claude/projects` 里出现过的 cwd(最近用过的项目) | P0 |
|
||||
| FR3 | 首页提供 **Projects 视图**(与现有 Sessions 选择器并列/可切换),渲染项目卡片网格 | P0 |
|
||||
| FR4 | 点项目卡片**主操作** → 新开 tab:cwd=仓库路径、自动运行 `claude`、tab 标签=repo 名 | P0 |
|
||||
| FR5 | **一个项目可对应多个 session**(1:N)。卡片展示该项目下**所有运行中的 session**(每个含名称/Claude 状态/缩略图),可分别进入;同时永远提供"+ 新开"。详见 §4.6 | P0 |
|
||||
| FR6 | 卡片显示 **当前分支** + **dirty 标记** | P1 |
|
||||
| FR7 | 项目搜索/过滤框;收藏/置顶(localStorage 持久化) | P1 |
|
||||
| FR8 | 「+ 新开」菜单:新开 `claude` / 只开 shell / `claude --resume <最近会话>` | P2 |
|
||||
| FR9 | 性能:发现逻辑有**深度上限**、跳过 `node_modules`/`.git` 内部、结果缓存,避免扫盘卡顿 | P0 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 设计与架构 (Design)
|
||||
|
||||
### 4.1 数据流
|
||||
|
||||
```
|
||||
首页 Projects 视图
|
||||
│ GET /projects
|
||||
▼
|
||||
src/http/projects.ts ──扫描配置根目录找 .git──► ProjectInfo[]
|
||||
│ ──合并 ~/.claude/projects 的 cwd──►
|
||||
│ ──对每个 repo 读 branch / dirty──►
|
||||
▼
|
||||
卡片网格 (public/projects.ts)
|
||||
│ 点卡片
|
||||
▼
|
||||
TabApp.openProject(repoPath, repoName)
|
||||
│ = addEntry(null, repoName, repoPath, 'claude') ← 全部已存在的能力
|
||||
▼
|
||||
TerminalSession: attach{cwd: repoPath} → spawn PTY(cwd) → 700ms 后自动输入 "claude\r"
|
||||
│
|
||||
▼
|
||||
tab 标签 = customTitle(repoName)(最高优先级,shell 之后发的 OSC 标题不会覆盖)
|
||||
```
|
||||
|
||||
**核心洞察**:从"点项目"到"开出带名字、跑着 claude 的 session"这条链路,**终端侧零改动**——直接调 `TabApp.addEntry(null, repoName, repoPath, 'claude')` 即可(见集成点表)。
|
||||
|
||||
### 4.2 既有集成点(无需改动,直接复用)
|
||||
|
||||
| 能力 | 现状 | 钩子位置 |
|
||||
|------|------|----------|
|
||||
| 按目录 spawn PTY | ✅ 已有 (M6) | `attach.cwd` → `manager.handleAttach(…, cwd)` → `createSession(…, cwd)` → `spawn({cwd})` (`src/session/session.ts:93`) |
|
||||
| 自动运行 `claude` | ✅ 已有 (O2, 客户端驱动) | `TerminalSessionOpts.initialInput`,`attached` 后 700ms 自动输入 (`public/terminal-session.ts:253-261`) |
|
||||
| 标签=repo 名 | ✅ 已有 | `addEntry(null, repoName, repoPath, 'claude')`;`customTitle` 优先级最高 (`public/tabs.ts:107,143`) |
|
||||
| 启动器视图骨架 | ✅ 可复用 | `mountLauncher` / `.mg-grid` / `makePreviewCard` (`public/launcher.ts`, `public/preview-grid.ts`) |
|
||||
| 历史 cwd 扫描模板 | ✅ 可复用 | `listSessions()` 扫 `~/.claude/projects/*.jsonl`,提取 cwd (`src/http/history.ts:80`) |
|
||||
|
||||
### 4.3 新增后端:`src/http/projects.ts`(模仿 `history.ts`)
|
||||
|
||||
```ts
|
||||
// 在 src/types.ts 增加共享契约(协调点,不要本地重声明)
|
||||
export interface ProjectInfo {
|
||||
name: string; // repo 目录名
|
||||
path: string; // 绝对路径
|
||||
isGit: boolean;
|
||||
branch?: string; // 当前分支(git 仓库才有)
|
||||
dirty?: boolean; // 有无未提交改动
|
||||
lastActiveMs?: number; // 来自 ~/.claude/projects 的最近 mtime,用于排序
|
||||
sessions: ProjectSessionRef[]; // 该项目下当前运行中的 session(1:N,可能为空)
|
||||
}
|
||||
|
||||
// 一个项目下的某个运行中 session 的引用(由 /projects 把 live-sessions 按 cwd 归并而来)
|
||||
export interface ProjectSessionRef {
|
||||
id: string; // sessionId
|
||||
title?: string; // tab 标题 / 自动名(如 'claude', 'shell', 'test')
|
||||
claudeStatus?: 'working' | 'waiting' | 'idle'; // 复用 v0.3 hook 状态
|
||||
startedMs: number;
|
||||
clients: number; // 当前挂着几个 WS 客户端(多设备镜像)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/http/projects.ts (纯异步、best-effort、parse 逻辑可单测)
|
||||
export async function listProjects(cfg): Promise<ProjectInfo[]>
|
||||
// 1. roots = cfg.projectRoots(默认 [os.homedir()])
|
||||
// 2. 广度优先扫描,遇到 .git 即记为 repo 并停止下钻该子树
|
||||
// - 深度上限 cfg.projectScanDepth(默认 4)
|
||||
// - 跳过 node_modules / .git / 隐藏目录 / 符号链接环
|
||||
// 3. 合并 history.ts 收集到的 cwd(最近用过但不在扫描根下的项目)
|
||||
// 4. 对每个 repo:读分支 = 解析 .git/HEAD(轻量,免 spawn git);
|
||||
// dirty = `git status --porcelain` 截断输出(可选、可惰性/并发限流)
|
||||
// 5. 去重(by path)、按 lastActiveMs 倒序、限量返回
|
||||
```
|
||||
|
||||
> **分支读取**:优先 `读取 .git/HEAD`(`ref: refs/heads/<branch>`)避免为每个 repo spawn git,扫描快。dirty 状态较贵(要 `git status`),可**惰性**(卡片展开/hover 时再查)或**并发限流**(一次最多 N 个)以免大量 repo 卡顿。
|
||||
|
||||
### 4.4 新增端点:`GET /projects`
|
||||
|
||||
只读发现端点,**与 `/sessions`、`/live-sessions` 同类**——不需要 Origin/CSRF 守卫(那是给状态变更路由的)。放在 `src/server.ts:151` 附近:
|
||||
|
||||
```ts
|
||||
app.get('/projects', async (_req, res) => res.json(await listProjects(cfg)));
|
||||
```
|
||||
|
||||
### 4.5 前端:`public/projects.ts` + `TabApp.openProject`
|
||||
|
||||
- `mountProjects(host, { onOpenProject, onEnterSession })`,与 `mountLauncher` 并列挂载(`tabs.ts:59` 旁)。
|
||||
- 首页加 **Sessions ↔ Projects 切换**(两个 tab/段控),复用 `.mg-grid` 与 `el()` 卡片工具。
|
||||
- 项目卡片:repo 名 + 分支 chip + dirty 圆点 + **session 计数徽标 + 该项目所有 running session 列表**(见 §4.6)。
|
||||
- `openProject`(新开)与 `openSession`(进入既有,已存在于 `tabs.ts:129`):
|
||||
|
||||
```ts
|
||||
// public/tabs.ts 新增(与 newTabForResume 几乎同构)
|
||||
openProject(repoPath: string, repoName: string, cmd = 'claude') {
|
||||
// 同名多开时加序号后缀,避免标签全叫 'web-terminal'
|
||||
const n = this.countOpenWithTitlePrefix(repoName);
|
||||
const label = n === 0 ? repoName : `${repoName} #${n + 1}`;
|
||||
this.addEntry(null, label, repoPath, cmd); // customTitle, cwd, initialInput
|
||||
this.rebuild(); // + activate 新 tab
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.6 一个项目 ↔ 多个 session(1:N,核心修正)
|
||||
|
||||
一个项目常会同时存在多个 session:**一个 `claude` + 一个跑测试/`git` 的 shell**、或**并行开几个 claude 比方案**(Conductor/Crystal 模式)、或同一会话被**多设备镜像**。因此卡片不能假设"0 或 1 个会话"。
|
||||
|
||||
**归并逻辑(后端 `listProjects`)**:把 `manager.list()`(`GET /live-sessions`,每个含 `cwd`、`clients`,见 `session.ts:116`)按 **cwd 前缀归属到项目**,填进 `ProjectInfo.sessions[]`;Claude 状态来自 v0.3 hook 侧信道(按 sessionId)。
|
||||
|
||||
> **匹配口径**:以 session 的**spawn cwd** 为锚(稳定,不随用户 `cd` 漂移)。`cwd === project.path` 或 `cwd` 在 `project.path` 之下都算该项目。**git worktree**(同 repo、不同路径)v0.6 先按各自路径当**独立项目**呈现;"按 repo 把多个 worktree 收拢成一组"列入未来扩展(§9)。
|
||||
|
||||
**卡片交互(按 session 数自适应)**:
|
||||
|
||||
| 该项目 running session 数 | 卡片主体 | 主操作 |
|
||||
|---|---|---|
|
||||
| **0** | "+ start claude here" 占位 | **Open ▸ claude**(`openProject`) |
|
||||
| **1** | 1 个 session 行(状态点 + 名称 + 缩略图) | 点行 = 进入;底部 **+ New** |
|
||||
| **N** | N 个 session 行(各带 ⚙/⏳/✓ 状态、镜像端数、可单独进入/Kill) | 每行点击进入对应 session;底部 **+ New claude ▾** |
|
||||
|
||||
- 每个 session 行:`● 状态点` + `标题`(claude / shell / `#2`…)+ `👥×2`(镜像设备数)+ 点击 → `onEnterSession(id)` = `TabApp.openSession(id)`(已有,会聚焦既有 tab 或挂载该 session,不重复开)。
|
||||
- 行尾 `✕` = Kill 该 session(复用 `DELETE /live-sessions/:id`,带 Origin 守卫)。
|
||||
- 「+ New」永远在:因为"已有会话"不代表"不想再开一个"。`▾` 展开 = 新 claude / 只开 shell / `claude --resume`。
|
||||
- **同名多 tab**:`openProject` 给第 2+ 个加 `#n` 后缀(如 `web-terminal #2`),避免标签无法区分。
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全考量 (Security)
|
||||
|
||||
- `GET /projects` 只读,符合现有威胁模型(工具已对 LAN 交出整个 shell,暴露目录名不额外升级风险)——与 `/sessions` 一致,**不加** Origin 守卫,但**也不能写**。
|
||||
- **路径越界**:发现的路径仅在服务端本地枚举;前端传回的 `cwd` 走既有 `attach.cwd` 校验(必须绝对路径,`protocol.ts:142`),不引入新攻击面。
|
||||
- **扫描成本即 DoS 面**:深度上限 + 跳过大目录 + 结果缓存(如 10s TTL),避免被频繁刷 `/projects` 拖垮磁盘。
|
||||
- dirty 检查用 `git status` 会 spawn 子进程——**限流 + 超时**,防止 repo 多时 fork 爆炸。
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置 (新增 env,沿用"无硬编码"原则)
|
||||
|
||||
| Env | 默认 | 说明 |
|
||||
|-----|------|------|
|
||||
| `PROJECT_ROOTS` | `~`(home 目录) | 逗号分隔的扫描根目录 |
|
||||
| `PROJECT_SCAN_DEPTH` | `4` | 扫描最大深度 |
|
||||
| `PROJECT_SCAN_TTL` | `10000` (ms) | `/projects` 结果缓存时长 |
|
||||
| `PROJECT_DIRTY_CHECK` | `1` | 是否计算 dirty(0=关,省 git 调用) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 任务拆解 (v0.6 — 按 PLAN/Owns 模型)
|
||||
|
||||
> 沿用 v0.4/v0.5 的做法:作为 feature 条目并入,定义 disjoint `Owns:` 便于并行派给 `module-builder`。
|
||||
|
||||
| 任务 | Owns(独占写) | 依赖 | 说明 |
|
||||
|------|----------------|------|------|
|
||||
| **P1 类型契约** | `src/types.ts`(加 `ProjectInfo`,改 `Config` 加 project* 字段) | — | 协调点,先落地,解锁并行 |
|
||||
| **P2 后端发现** | `src/http/projects.ts` + `test/projects.test.ts` | P1 | git 扫描 + HEAD/dirty + 合并 history cwd + 缓存(TDD,parse 纯函数单测) |
|
||||
| **P3 配置** | `src/config.ts`(加 `projectRoots/scanDepth/...`) | P1 | env 解析 + 默认值 |
|
||||
| **P4 端点接线** | `src/server.ts`(加 `GET /projects` 一行 + import) | P2,P3 | 与 `/sessions` 同位 |
|
||||
| **P5 前端面板** | `public/projects.ts` + 样式 | P1 | 卡片网格、搜索、收藏、切换视图 |
|
||||
| **P6 Tab 接线** | `public/tabs.ts`(`openProject` + 挂载 `mountProjects` + running-session 匹配) | P5 | 复用 `addEntry`/`openSession` |
|
||||
|
||||
> P2/P3/P5 同波次、文件 disjoint,可并行;P4/P6 收尾接线。建议 builder 用 `isolation: worktree`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收标准 (Acceptance)
|
||||
|
||||
- **A1**:`GET /projects` 在含多个 git 仓库的目录树下返回正确列表(名称/路径/分支/dirty),扫描有深度上限、跳过 `node_modules`,10s 内缓存命中。
|
||||
- **A2**:首页能切到 Projects 视图,渲染卡片网格;分支与 dirty 显示正确。
|
||||
- **A3**:点一个**无运行会话**的项目 → 新 tab 标签=repo 名、cwd 正确、约 0.7s 后自动出现 `claude` 启动界面。
|
||||
- **A4**:点一个**已有运行会话**的项目 → 进入既有 session,不重复开。
|
||||
- **A5**:搜索过滤、收藏置顶生效并持久化。
|
||||
- **A6**:后端测试覆盖发现/解析逻辑(≥80%,纯函数单测 + 临时目录集成)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 未来扩展 / 暂不做 (Out of Scope for v0.6)
|
||||
|
||||
- **git worktree 看板**(Conductor/Crystal 式:一个项目并行多 worktree 多 agent)——强大但属另一个大特性,v0.6 先做"一项目一会话"。
|
||||
- 项目内**文件树/编辑器/Git Explorer**(CloudCLI 有)——与本项目 byte-shuttle 哲学冲突,明确不做。
|
||||
- 从 ticket/issue **拉任务进项目**(Conductor 式)——后续可作为 v0.7 与 hook 系统结合。
|
||||
- 多 agent 中立(Cursor/Codex/Gemini)——目前 hooks 与 `claude` 命令是 Claude 专用;如需中立化,`initialInput` 可改为可配置命令。
|
||||
|
||||
---
|
||||
|
||||
## 附:一句话总结
|
||||
|
||||
> 这是一个**"发现源 + 启动器视图"**特性。开 session 的终端机制(按目录 spawn、自动跑 claude、标签=repo 名)**今天就能做到**,只差**仓库发现(`GET /projects`)**和**项目面板 UI**两块新代码。
|
||||
@@ -24,6 +24,20 @@
|
||||
|
||||
> 新会话读到的第一块。保持准确,只描述"此刻"。
|
||||
|
||||
### 🚧 v0.6 Project Manager 项目工作台(进行中 — 2026-06-30 交接)
|
||||
|
||||
- **设计/计划已就绪**: `docs/FEATURE_PROJECT_MANAGER.md`(含 1:N 多 session 设计)、`docs/THEME.md`(Amber 主题)、`docs/mockups/`(预览图,`final-amber.png` 为定稿视觉)。计划见会话内 `/ecc:plan` 输出(wave 分波 + Owns)。
|
||||
- **主题已 ship**: `public/style.css` 靛蓝 `#7c8cff` → **Amber 琥珀金 `#e3a64a`**;新增 `--on-accent`;暖灰底;`--green/--amber/--red` 状态色保留;keybar 残留靛蓝已清。
|
||||
- **地基已落(编译绿,未提交)**:
|
||||
- ✅ **T-PM1 契约** `src/types.ts`: 新增 `ProjectInfo` / `ProjectSessionRef`(复用 `ClaudeStatus`) + Config 4 字段(`projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck`)。
|
||||
- ✅ **T-PM3 配置** `src/config.ts`: `parseBool` / `parseProjectRoots` helper + 解析 4 个 env(`PROJECT_ROOTS`/`PROJECT_SCAN_DEPTH`/`PROJECT_SCAN_TTL`/`PROJECT_DIRTY_CHECK`,默认 `[homeDir]`/4/10000/true)。`npm run typecheck` 通过。
|
||||
- **下一步(重启后从这里继续)**:
|
||||
1. **W1 并行 2 builder(worktree)**: **T-PM2** 后端发现 `src/http/projects.ts` + `test/projects.test.ts`(`buildProjects(cfg, liveSessions)`:扫 .git→分支/dirty→合并 ~/.claude/projects cwd→按 cwd 归并 sessions→缓存,镜像 `src/http/history.ts:80` 模式);**T-PM5** 前端面板 `public/projects.ts` + style 追加(`mountProjects`,1:N 卡片,复用 launcher/`el()`/Amber token)。
|
||||
2. **W2 并行 2**: **T-PM4** `src/server.ts` 加 `GET /projects`(只读,无 Origin 守卫,喂 `manager.list()`);**T-PM6** `public/tabs.ts` `openProject`(同名加 `#n`,复用 `addEntry`)+ 挂 `mountProjects` + 首页 Sessions↔Projects 段控。
|
||||
3. **W3**: `module-reviewer` 验收 A1–A6(report-only)。
|
||||
- 关键接口约定: `buildProjects(cfg, liveSessions): Promise<ProjectInfo[]>`(注入 liveSessions 便于纯测)。
|
||||
- **注意**: 当前工作树有未提交改动(types.ts/config.ts/style.css/docs)。建议重启后先 `git status` 核对,按规范建 `v0.6-projects` 分支再继续。
|
||||
|
||||
- **当前阶段**: **v0.3 全部完成**(H1–H4 + M3/M6/M7 已并入 main;**O2 历史浏览** 在分支 `o2-history`,待合并)。
|
||||
- ✅ Step1 前端快赢 · ✅ H2/H4 状态感知 · ✅ H3 远程批准 · ✅ H1 tmux 保活 · ✅ M3/M7/M6 · ✅ **O2 历史会话浏览/resume** · ✅ tech-debt 清理(@ts-ignore 去掉)· ✅ UI:终端不再打印 Connecting/Connected(靠标签点)。
|
||||
- ✅ **现代化 UI 主题**(design-token 调色板/靛蓝强调色/圆角芯片标签+键栏/毛玻璃弹层)· ✅ **键栏功能字幕**(每键下方标作用)+ 新增 ^R/^L/^D(共 17 键)· ✅ **⌨ 快捷键速查弹层**(`public/shortcuts.ts`)。
|
||||
|
||||
50
docs/THEME.md
Normal file
50
docs/THEME.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Theme — Amber 琥珀金(设计 token 源)
|
||||
|
||||
> 全站视觉的**单一事实来源**。强调色全部来自 `--accent / --accent-2 / --accent-soft`,
|
||||
> 改这几个 token 即可整体换肤。定义在 `public/style.css` 的 `:root`。
|
||||
> 选型经过 10 套候选对比(见 `docs/mockups/palette-compare*.png`),最终定为 **Amber 琥珀金**:
|
||||
> 暖中性底 + 金色强调,暗色护眼、长时间看不累,且明确避开蓝/紫/绿。
|
||||
|
||||
## 调色板 (Design Tokens)
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|-------|-----|------|
|
||||
| `--bg` | `#100f0d` | 终端 / 最深表面(暖中性,非冷蓝灰) |
|
||||
| `--surface-1` | `#181613` | 标签栏、键栏 |
|
||||
| `--surface-2` | `#1f1c17` | 标签、按钮、卡片 |
|
||||
| `--surface-3` | `#2a2620` | hover / active |
|
||||
| `--border` | `rgba(255,255,255,.07)` | 普通描边 |
|
||||
| `--border-strong` | `rgba(255,255,255,.14)` | 强描边 |
|
||||
| `--text` | `#ece9e3` | 主文字(暖白) |
|
||||
| `--text-dim` | `#a8a299` | 次要文字 |
|
||||
| `--text-faint` | `#6f6a61` | 弱化文字 |
|
||||
| **`--accent`** | **`#e3a64a`** | **强调色:金色**(按钮、链接、选中、聚焦环) |
|
||||
| `--accent-2` | `#c9892f` | 深金(渐变 / hover) |
|
||||
| `--accent-soft` | `rgba(227,166,74,.15)` | 强调色半透明(选中底、chip、聚焦光晕) |
|
||||
| `--on-accent` | `#1a1305` | **金色底上的文字色**(深墨,保证对比度——金底配白字会糊) |
|
||||
|
||||
## 状态色(语义独立,勿与强调色混用)
|
||||
|
||||
连接状态点沿用既有语义,**不随主题强调色变**:
|
||||
|
||||
| Token | 值 | 含义 |
|
||||
|-------|-----|------|
|
||||
| `--green` | `#46d07f` | 已连接 connected |
|
||||
| `--amber` | `#f5b14c` | 连接中 connecting |
|
||||
| `--red` | `#ff6b6b` | 断开 / 出错 disconnected / error |
|
||||
|
||||
> Project 面板里的 session 状态语义(v0.6):`working = --accent 金` · `waiting = --amber 琥珀(闪)` · `idle = 中性灰 #8b8a86` · `dirty = 暖琥珀`。
|
||||
|
||||
## 接入约定(改主题只改这里)
|
||||
|
||||
- 任何"强调"视觉 **只用 token**,不要写死颜色(`#7c8cff` 这类硬编码已清除)。
|
||||
- **金色当背景时,文字必须用 `var(--on-accent)`**(深墨),不要用 `#fff`。
|
||||
当前 4 处 accent 背景按钮已统一改为 `--on-accent`:`.settings-opt.active` / `.hist-resume` 等。
|
||||
- 换肤 = 替换 `:root` 的 `--accent / --accent-2 / --accent-soft / --on-accent`(必要时连 `--bg/--surface-*` 一起)。
|
||||
- 历史:原 indigo 靛蓝 `#7c8cff` 已于本次替换为 Amber(git 可回溯)。
|
||||
|
||||
## 候选存档
|
||||
|
||||
10 套对比预览(暖底、无蓝/紫/绿):`docs/mockups/palette-compare.png`(Amber/Copper/Mono/Rose)、
|
||||
`docs/mockups/palette-compare-2.png`(Ruby/Sand/Brass/Apricot/Slate/Clay)。
|
||||
最终全面板预览:`docs/mockups/final-amber.png`。
|
||||
BIN
docs/mockups/final-amber.png
Normal file
BIN
docs/mockups/final-amber.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 565 KiB |
BIN
docs/mockups/final-clay.png
Normal file
BIN
docs/mockups/final-clay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 734 KiB |
BIN
docs/mockups/final-sand.png
Normal file
BIN
docs/mockups/final-sand.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 565 KiB |
95
docs/mockups/palette-compare-2.html
Normal file
95
docs/mockups/palette-compare-2.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="zh"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Palette compare 2</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#100f0d; --card:#1b1916; --border:rgba(255,255,255,.07);
|
||||
--border-strong:rgba(255,255,255,.14);
|
||||
--text:#ece9e3; --text-dim:#a8a299; --text-faint:#6f6a61;
|
||||
--idle:#8b8a86;
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
--ui:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font-family:var(--ui);padding:26px}
|
||||
h2{font-weight:680;font-size:18px;margin:0 0 4px}
|
||||
.lead{color:var(--text-faint);font-size:13px;margin:0 0 22px}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
||||
|
||||
.col[data-p="ruby"] {--accent:#d6455d;--soft:rgba(214,69,93,.16);--line:rgba(214,69,93,.40);--ink:#fff;--working:#d6455d;--waiting:#e6b84e}
|
||||
.col[data-p="sand"] {--accent:#cdb486;--soft:rgba(205,180,134,.14);--line:rgba(205,180,134,.36);--ink:#1a1408;--working:#cdb486;--waiting:#e0975a}
|
||||
.col[data-p="brass"] {--accent:#d9b441;--soft:rgba(217,180,65,.15);--line:rgba(217,180,65,.38);--ink:#1a1404;--working:#d9b441;--waiting:#e08a3c}
|
||||
.col[data-p="apricot"]{--accent:#f0a878;--soft:rgba(240,168,120,.15);--line:rgba(240,168,120,.38);--ink:#1c1109;--working:#f0a878;--waiting:#e6c24e}
|
||||
.col[data-p="slate"] {--accent:#9aa3ab;--soft:rgba(154,163,171,.13);--line:rgba(154,163,171,.32);--ink:#12140f;--working:#aab2b9;--waiting:#e3a64a}
|
||||
.col[data-p="clay"] {--accent:#cf6b4f;--soft:rgba(207,107,79,.15);--line:rgba(207,107,79,.38);--ink:#fff;--working:#cf6b4f;--waiting:#e0aa4e}
|
||||
|
||||
.swatch{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:11px;
|
||||
border:1px solid var(--border);background:var(--card)}
|
||||
.swatch .chipc{width:22px;height:22px;border-radius:7px;background:var(--accent)}
|
||||
.swatch .nm{font-weight:680;font-size:14px}
|
||||
.swatch .hex{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--text-faint)}
|
||||
.col{display:flex;flex-direction:column;gap:12px}
|
||||
|
||||
.card{background:linear-gradient(180deg,var(--card),#161410);border:1px solid var(--line);
|
||||
border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:10px;position:relative;overflow:hidden;
|
||||
box-shadow:0 12px 40px rgba(0,0,0,.5)}
|
||||
.card::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.85}
|
||||
.crow{display:flex;align-items:center;gap:8px}
|
||||
.name{font-weight:680;font-size:14px}
|
||||
.star{color:var(--waiting);font-size:12px}
|
||||
.countbadge{margin-left:auto;font-size:10.5px;padding:3px 9px;border-radius:999px;
|
||||
background:var(--soft);color:var(--accent);display:flex;align-items:center;gap:5px;font-weight:650}
|
||||
.countbadge .d{width:6px;height:6px;border-radius:50%;background:var(--accent)}
|
||||
.meta{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.chip{font-size:10.5px;padding:3px 8px;border-radius:999px;border:1px solid var(--border);
|
||||
color:var(--text-dim);font-family:var(--mono)}
|
||||
.chip.dirty{color:var(--waiting);border-color:var(--line)}
|
||||
.path{font-family:var(--mono);font-size:10.5px;color:var(--text-faint)}
|
||||
.srow{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;
|
||||
background:rgba(255,255,255,.025);border:1px solid var(--border);position:relative}
|
||||
.srow::before{content:"";position:absolute;left:0;top:7px;bottom:7px;width:2px;border-radius:2px;background:var(--rail)}
|
||||
.srow.w{--rail:var(--working)} .srow.q{--rail:var(--waiting)} .srow.i{--rail:var(--idle)}
|
||||
.st{width:8px;height:8px;border-radius:50%;margin-left:3px}
|
||||
.st.w{background:var(--working);box-shadow:0 0 8px var(--working)}
|
||||
.st.q{background:var(--waiting);box-shadow:0 0 8px var(--waiting)}
|
||||
.st.i{background:var(--idle)}
|
||||
.stitle{font-size:12.5px;font-weight:600}
|
||||
.smeta{font-size:10.5px;color:var(--text-faint);font-family:var(--mono)}
|
||||
.x{margin-left:auto;color:var(--text-faint);font-size:12px}
|
||||
.actions{display:flex;gap:7px}
|
||||
.btn{flex:1;text-align:center;font-size:11.5px;font-weight:680;padding:9px;border-radius:9px;
|
||||
background:var(--accent);color:var(--ink)}
|
||||
.btn.new{background:var(--soft);color:var(--accent);box-shadow:inset 0 0 0 1px var(--line)}
|
||||
.ghost{padding:9px 12px;border-radius:9px;border:1px solid var(--border);color:var(--text-dim);font-size:11.5px}
|
||||
</style></head>
|
||||
<body>
|
||||
<h2>再来 6 套强调色(暖灰底不变,无蓝/紫/绿)</h2>
|
||||
<p class="lead">working=强调色 · waiting=琥珀(闪) · idle=中性灰 · dirty=暖琥珀</p>
|
||||
<div class="row" id="root"></div>
|
||||
<script>
|
||||
const PALS = [
|
||||
['ruby','宝石红 Ruby','#d6455d'],
|
||||
['sand','沙褐 Sand','#cdb486'],
|
||||
['brass','黄铜 Brass','#d9b441'],
|
||||
['apricot','杏桃 Apricot','#f0a878'],
|
||||
['slate','冷石灰 Slate','#9aa3ab'],
|
||||
['clay','陶土 Clay','#cf6b4f'],
|
||||
];
|
||||
document.getElementById('root').innerHTML = PALS.map(([p,nm,hex])=>`
|
||||
<div class="col" data-p="${p}">
|
||||
<div class="swatch"><span class="chipc"></span><span class="nm">${nm}</span><span class="hex">${hex}</span></div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty">dirty</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="srow w"><span class="st w"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="x">✕</span></div>
|
||||
<div class="srow q"><span class="st q"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting</span><span class="x">✕</span></div>
|
||||
<div class="srow i"><span class="st i"></span><span class="stitle">shell</span><span class="smeta">✓ idle</span><span class="x">✕</span></div>
|
||||
<div class="actions"><span class="btn new">+ New claude</span><span class="ghost">▾</span></div>
|
||||
<div class="actions"><span class="btn">Open ▸ claude</span></div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
docs/mockups/palette-compare-2.png
Normal file
BIN
docs/mockups/palette-compare-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 700 KiB |
132
docs/mockups/palette-compare.html
Normal file
132
docs/mockups/palette-compare.html
Normal file
@@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html lang="zh"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Palette compare</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#100f0d; --card:#1b1916; --border:rgba(255,255,255,.07);
|
||||
--border-strong:rgba(255,255,255,.14);
|
||||
--text:#ece9e3; --text-dim:#a8a299; --text-faint:#6f6a61;
|
||||
--idle:#8b8a86; --red:#ff6b6b;
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
--ui:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font-family:var(--ui);padding:26px}
|
||||
h2{font-weight:680;font-size:18px;margin:0 0 4px}
|
||||
.lead{color:var(--text-faint);font-size:13px;margin:0 0 22px}
|
||||
.row{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
|
||||
|
||||
/* per-column palette via data-attr */
|
||||
.col{display:flex;flex-direction:column;gap:12px}
|
||||
.col[data-p="amber"] {--accent:#e3a64a;--accent-2:#c9892f;--soft:rgba(227,166,74,.15);--line:rgba(227,166,74,.38);--ink:#1a1305;--working:#e3a64a;--waiting:#7fb2ff00;--waiting:#d98b3a;--wait2:#ffd27a}
|
||||
.col[data-p="copper"] {--accent:#e8743b;--accent-2:#c95a27;--soft:rgba(232,116,59,.15);--line:rgba(232,116,59,.38);--ink:#1c0e06;--working:#e8743b;--waiting:#e6b84e;--wait2:#ffcf6e}
|
||||
.col[data-p="mono"] {--accent:#ece9e3;--accent-2:#bdb8ad;--soft:rgba(236,233,227,.10);--line:rgba(236,233,227,.30);--ink:#15130f;--working:#ece9e3;--waiting:#e3a64a;--wait2:#ffd27a}
|
||||
.col[data-p="rose"] {--accent:#ef6f73;--accent-2:#cf5358;--soft:rgba(239,111,115,.15);--line:rgba(239,111,115,.38);--ink:#1c0809;--working:#ef6f73;--waiting:#e6b84e;--wait2:#ffcf6e}
|
||||
|
||||
.swatch{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:11px;
|
||||
border:1px solid var(--border);background:var(--card)}
|
||||
.swatch .chipc{width:22px;height:22px;border-radius:7px;background:var(--accent)}
|
||||
.swatch .nm{font-weight:680;font-size:14px}
|
||||
.swatch .hex{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--text-faint)}
|
||||
|
||||
.card{background:linear-gradient(180deg,var(--card),#161410);border:1px solid var(--line);
|
||||
border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:10px;position:relative;overflow:hidden;
|
||||
box-shadow:0 12px 40px rgba(0,0,0,.5)}
|
||||
.card::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.85}
|
||||
.crow{display:flex;align-items:center;gap:8px}
|
||||
.name{font-weight:680;font-size:14px}
|
||||
.star{color:var(--waiting);font-size:12px}
|
||||
.countbadge{margin-left:auto;font-size:10.5px;padding:3px 9px;border-radius:999px;
|
||||
background:var(--soft);color:var(--accent);display:flex;align-items:center;gap:5px;font-weight:650}
|
||||
.countbadge .d{width:6px;height:6px;border-radius:50%;background:var(--accent)}
|
||||
.meta{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.chip{font-size:10.5px;padding:3px 8px;border-radius:999px;border:1px solid var(--border);
|
||||
color:var(--text-dim);font-family:var(--mono)}
|
||||
.chip.dirty{color:var(--waiting);border-color:var(--line)}
|
||||
.path{font-family:var(--mono);font-size:10.5px;color:var(--text-faint)}
|
||||
.srow{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;
|
||||
background:rgba(255,255,255,.025);border:1px solid var(--border);position:relative}
|
||||
.srow::before{content:"";position:absolute;left:0;top:7px;bottom:7px;width:2px;border-radius:2px;background:var(--rail)}
|
||||
.srow.w{--rail:var(--working)} .srow.q{--rail:var(--waiting)} .srow.i{--rail:var(--idle)}
|
||||
.st{width:8px;height:8px;border-radius:50%;margin-left:3px}
|
||||
.st.w{background:var(--working);box-shadow:0 0 8px var(--working)}
|
||||
.st.q{background:var(--waiting);box-shadow:0 0 8px var(--waiting)}
|
||||
.st.i{background:var(--idle)}
|
||||
.stitle{font-size:12.5px;font-weight:600}
|
||||
.smeta{font-size:10.5px;color:var(--text-faint);font-family:var(--mono)}
|
||||
.x{margin-left:auto;color:var(--text-faint);font-size:12px}
|
||||
.actions{display:flex;gap:7px}
|
||||
.btn{flex:1;text-align:center;font-size:11.5px;font-weight:680;padding:9px;border-radius:9px;
|
||||
background:var(--accent);color:var(--ink)}
|
||||
.btn.new{background:var(--soft);color:var(--accent);box-shadow:inset 0 0 0 1px var(--line)}
|
||||
.ghost{padding:9px 12px;border-radius:9px;border:1px solid var(--border);color:var(--text-dim);font-size:11.5px}
|
||||
</style></head>
|
||||
<body>
|
||||
<h2>选个强调色 — 同一张卡片,4 套配色(暖灰底不变,无蓝/紫/绿)</h2>
|
||||
<p class="lead">working=强调色 · waiting=琥珀(闪) · idle=中性灰 · dirty=暖琥珀</p>
|
||||
<div class="row">
|
||||
|
||||
<!-- AMBER -->
|
||||
<div class="col" data-p="amber">
|
||||
<div class="swatch"><span class="chipc"></span><span class="nm">琥珀金 Amber</span><span class="hex">#e3a64a</span></div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty">dirty</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="srow w"><span class="st w"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="x">✕</span></div>
|
||||
<div class="srow q"><span class="st q"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting</span><span class="x">✕</span></div>
|
||||
<div class="srow i"><span class="st i"></span><span class="stitle">shell</span><span class="smeta">✓ idle</span><span class="x">✕</span></div>
|
||||
<div class="actions"><span class="btn new">+ New claude</span><span class="ghost">▾</span></div>
|
||||
</div>
|
||||
<div class="actions"><span class="btn">Open ▸ claude</span></div>
|
||||
</div>
|
||||
|
||||
<!-- COPPER -->
|
||||
<div class="col" data-p="copper">
|
||||
<div class="swatch"><span class="chipc"></span><span class="nm">赤陶橙 Copper</span><span class="hex">#e8743b</span></div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty">dirty</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="srow w"><span class="st w"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="x">✕</span></div>
|
||||
<div class="srow q"><span class="st q"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting</span><span class="x">✕</span></div>
|
||||
<div class="srow i"><span class="st i"></span><span class="stitle">shell</span><span class="smeta">✓ idle</span><span class="x">✕</span></div>
|
||||
<div class="actions"><span class="btn new">+ New claude</span><span class="ghost">▾</span></div>
|
||||
</div>
|
||||
<div class="actions"><span class="btn">Open ▸ claude</span></div>
|
||||
</div>
|
||||
|
||||
<!-- MONO -->
|
||||
<div class="col" data-p="mono">
|
||||
<div class="swatch"><span class="chipc"></span><span class="nm">暖色单色 Mono</span><span class="hex">#ece9e3</span></div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty">dirty</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="srow w"><span class="st w"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="x">✕</span></div>
|
||||
<div class="srow q"><span class="st q"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting</span><span class="x">✕</span></div>
|
||||
<div class="srow i"><span class="st i"></span><span class="stitle">shell</span><span class="smeta">✓ idle</span><span class="x">✕</span></div>
|
||||
<div class="actions"><span class="btn new">+ New claude</span><span class="ghost">▾</span></div>
|
||||
</div>
|
||||
<div class="actions"><span class="btn">Open ▸ claude</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ROSE -->
|
||||
<div class="col" data-p="rose">
|
||||
<div class="swatch"><span class="chipc"></span><span class="nm">玫瑰珊瑚 Rose</span><span class="hex">#ef6f73</span></div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty">dirty</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="srow w"><span class="st w"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="x">✕</span></div>
|
||||
<div class="srow q"><span class="st q"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting</span><span class="x">✕</span></div>
|
||||
<div class="srow i"><span class="st i"></span><span class="stitle">shell</span><span class="smeta">✓ idle</span><span class="x">✕</span></div>
|
||||
<div class="actions"><span class="btn new">+ New claude</span><span class="ghost">▾</span></div>
|
||||
</div>
|
||||
<div class="actions"><span class="btn">Open ▸ claude</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
docs/mockups/palette-compare.png
Normal file
BIN
docs/mockups/palette-compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
175
docs/mockups/project-manager-final.html
Normal file
175
docs/mockups/project-manager-final.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Project Manager — final</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#100f0d; --panel:#181613; --card:#1b1916;
|
||||
--border:rgba(255,255,255,.07); --border-strong:rgba(255,255,255,.14);
|
||||
--text:#ece9e3; --text-dim:#a8a299; --text-faint:#6f6a61;
|
||||
--idle:#8b8a86; --red:#ff6b6b;
|
||||
--shadow:0 12px 44px rgba(0,0,0,.55); --radius:9px; --radius-lg:14px;
|
||||
--ui-font:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
/* default = amber */
|
||||
--accent:#e3a64a;--accent-2:#c9892f;--soft:rgba(227,166,74,.14);--line:rgba(227,166,74,.36);
|
||||
--ink:#1a1305;--working:#e3a64a;--waiting:#e6b84e;--dirty:#e0975a;--glow:rgba(227,166,74,.10);
|
||||
}
|
||||
body[data-p="sand"]{--accent:#cdb486;--accent-2:#b39c6e;--soft:rgba(205,180,134,.13);--line:rgba(205,180,134,.34);
|
||||
--ink:#1a1408;--working:#cdb486;--waiting:#e0975a;--dirty:#d9a26a;--glow:rgba(205,180,134,.09)}
|
||||
body[data-p="clay"]{--accent:#cf6b4f;--accent-2:#b1543b;--soft:rgba(207,107,79,.15);--line:rgba(207,107,79,.38);
|
||||
--ink:#ffffff;--working:#cf6b4f;--waiting:#e0aa4e;--dirty:#dd9a55;--glow:rgba(207,107,79,.10)}
|
||||
body[data-p="brass"]{--accent:#d9b441;--accent-2:#bd9a2c;--soft:rgba(217,180,65,.15);--line:rgba(217,180,65,.38);
|
||||
--ink:#1a1404;--working:#d9b441;--waiting:#e08a3c;--dirty:#dca64a;--glow:rgba(217,180,65,.10)}
|
||||
body[data-p="slate"]{--accent:#9aa3ab;--accent-2:#7f888f;--soft:rgba(154,163,171,.12);--line:rgba(154,163,171,.30);
|
||||
--ink:#12140f;--working:#aab2b9;--waiting:#e3a64a;--dirty:#cfa45e;--glow:rgba(154,163,171,.08)}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:radial-gradient(1100px 520px at 82% -12%, var(--glow), transparent 62%),var(--bg);
|
||||
color:var(--text);font-family:var(--ui-font);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||||
|
||||
.topbar{height:50px;display:flex;align-items:center;gap:14px;padding:0 18px;
|
||||
border-bottom:1px solid var(--border);background:rgba(24,22,19,.62);backdrop-filter:blur(14px);
|
||||
position:sticky;top:0;z-index:5}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:650;letter-spacing:.2px}
|
||||
.brand .dot{width:11px;height:11px;border-radius:50%;
|
||||
background:conic-gradient(from 200deg,var(--accent),var(--accent-2),var(--accent))}
|
||||
.seg{display:flex;background:#0c0b09;border:1px solid var(--border);border-radius:999px;padding:3px;gap:2px;margin-left:6px}
|
||||
.seg button{all:unset;cursor:pointer;font-size:13px;color:var(--text-dim);padding:5px 15px;border-radius:999px;
|
||||
transition:.15s;display:flex;align-items:center;gap:6px}
|
||||
.seg button.active{background:var(--soft);color:var(--accent);box-shadow:inset 0 0 0 1px var(--line)}
|
||||
.seg .mini{font-size:10px;background:var(--accent);color:var(--ink);border-radius:999px;padding:1px 6px;font-weight:700}
|
||||
.spacer{flex:1}
|
||||
.icns{display:flex;gap:6px}
|
||||
.icns button{all:unset;cursor:pointer;width:33px;height:33px;border-radius:8px;display:grid;place-items:center;
|
||||
color:var(--text-dim);font-size:15px;transition:.15s}
|
||||
.icns button:hover{background:var(--soft);color:var(--text)}
|
||||
|
||||
.wrap{max-width:1100px;margin:0 auto;padding:26px 22px 60px}
|
||||
.head{display:flex;align-items:baseline;gap:12px;margin:4px 0 22px}
|
||||
.head h1{font-size:20px;margin:0;font-weight:680;letter-spacing:.2px}
|
||||
.head .sub{color:var(--text-faint);font-size:13px}
|
||||
.head .grow{flex:1}
|
||||
.search{display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--border);
|
||||
border-radius:999px;padding:8px 15px;width:260px;color:var(--text-faint);font-size:13px;transition:.15s}
|
||||
.search:focus-within{border-color:var(--line)}
|
||||
.search input{all:unset;flex:1;color:var(--text);font-size:13px}
|
||||
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:16px}
|
||||
.card{background:linear-gradient(180deg,var(--card),#161410);border:1px solid var(--border);
|
||||
border-radius:var(--radius-lg);padding:15px;display:flex;flex-direction:column;gap:11px;
|
||||
box-shadow:var(--shadow);transition:.18s;position:relative;overflow:hidden}
|
||||
.card.running{border-color:var(--line)}
|
||||
.card.running::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.85}
|
||||
.crow{display:flex;align-items:center;gap:8px;min-width:0}
|
||||
.name{font-weight:680;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.star{color:var(--waiting);font-size:13px}
|
||||
.badge{margin-left:auto;font-size:11px;padding:3px 10px;border-radius:999px;display:flex;align-items:center;gap:6px;white-space:nowrap}
|
||||
.badge.idle{background:rgba(255,255,255,.05);color:var(--text-faint)}
|
||||
.countbadge{margin-left:auto;font-size:11px;padding:3px 10px;border-radius:999px;background:var(--soft);
|
||||
color:var(--accent);display:flex;align-items:center;gap:6px;font-weight:600}
|
||||
.countbadge .d{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 9px var(--accent)}
|
||||
.meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
||||
.chip{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--border);color:var(--text-dim);
|
||||
display:flex;align-items:center;gap:5px;font-family:var(--mono)}
|
||||
.chip.dirty{color:var(--dirty);border-color:var(--line)}
|
||||
.chip .gd{width:6px;height:6px;border-radius:50%;background:var(--dirty)}
|
||||
.path{font-family:var(--mono);font-size:11px;color:var(--text-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-2px}
|
||||
|
||||
.sessions{display:flex;flex-direction:column;gap:7px}
|
||||
.srow{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:10px;
|
||||
background:rgba(255,255,255,.025);border:1px solid var(--border);cursor:pointer;transition:.15s;position:relative}
|
||||
.srow::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;background:var(--rail,transparent)}
|
||||
.srow:hover{background:var(--soft);border-color:var(--line)}
|
||||
.srow.working{--rail:var(--working)} .srow.waiting{--rail:var(--waiting)} .srow.idle{--rail:var(--idle)}
|
||||
.srow .st{width:8px;height:8px;border-radius:50%;flex:0 0 auto;margin-left:3px}
|
||||
.st.working{background:var(--working);box-shadow:0 0 9px var(--working)}
|
||||
.st.waiting{background:var(--waiting);box-shadow:0 0 9px var(--waiting);animation:blink 1.1s steps(2) infinite}
|
||||
.st.idle{background:var(--idle)}
|
||||
@keyframes blink{50%{opacity:.25}}
|
||||
.srow .stitle{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.srow .smeta{font-size:11px;color:var(--text-faint);font-family:var(--mono);white-space:nowrap}
|
||||
.srow .mir{font-size:10px;color:var(--text-dim);background:rgba(255,255,255,.06);padding:2px 7px;border-radius:999px;white-space:nowrap}
|
||||
.srow .x{color:var(--text-faint);font-size:13px;padding:2px 5px;border-radius:6px;transition:.12s}
|
||||
.srow .x:hover{color:var(--red);background:rgba(255,107,107,.13)}
|
||||
.spush{margin-left:auto}
|
||||
|
||||
.thumb.empty{height:96px;border-radius:10px;border:1px dashed var(--border-strong);
|
||||
background:rgba(255,255,255,.015);display:grid;place-items:center;color:var(--text-faint)}
|
||||
.thumb.empty .plus{font-size:13px;color:var(--accent);border:1px dashed var(--line);padding:7px 16px;border-radius:999px}
|
||||
|
||||
.actions{display:flex;gap:8px;margin-top:2px}
|
||||
.btn{all:unset;cursor:pointer;flex:1;text-align:center;font-size:12px;font-weight:680;padding:9px;border-radius:10px;
|
||||
background:var(--accent);color:var(--ink);transition:.15s}
|
||||
.btn:hover{filter:brightness(1.07)}
|
||||
.btn.new{background:var(--soft);color:var(--accent);box-shadow:inset 0 0 0 1px var(--line)}
|
||||
.btn.shell{background:rgba(255,255,255,.07);color:var(--text)}
|
||||
.btn.ghost{flex:0 0 auto;padding:9px 13px;background:transparent;color:var(--text-dim);border:1px solid var(--border)}
|
||||
.btn.ghost:hover{border-color:var(--border-strong);color:var(--text)}
|
||||
.card.add{border-style:dashed;align-items:center;justify-content:center;color:var(--text-faint);min-height:236px;gap:6px;cursor:pointer}
|
||||
.card.add:hover{color:var(--text);border-color:var(--line)}
|
||||
.card.add .big{font-size:30px;color:var(--accent)}
|
||||
.note{margin-top:28px;color:var(--text-faint);font-size:12px;text-align:center;line-height:1.7}
|
||||
</style>
|
||||
</head>
|
||||
<body data-p="amber">
|
||||
<div class="topbar">
|
||||
<div class="brand"><span class="dot"></span> web-terminal</div>
|
||||
<div class="seg"><button>Sessions</button><button class="active">Projects <span class="mini">2</span></button></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="icns"><button>⚙</button><button>🗂</button><button>⌨</button></div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<h1>Projects</h1><span class="sub" id="palname">Amber</span>
|
||||
<span class="grow"></span>
|
||||
<div class="search">🔍 <input placeholder="search projects…"></div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card running">
|
||||
<div class="crow"><span class="star">★</span><span class="name">web-terminal</span><span class="countbadge"><span class="d"></span>3 sessions</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip dirty"><span class="gd"></span>dirty</span><span class="chip">2h ago</span></div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="sessions">
|
||||
<div class="srow working"><span class="st working"></span><span class="stitle">claude</span><span class="smeta">⚙ working</span><span class="mir spush">👥 2</span><span class="x">✕</span></div>
|
||||
<div class="srow waiting"><span class="st waiting"></span><span class="stitle">claude #2</span><span class="smeta">⏳ waiting approval</span><span class="x spush">✕</span></div>
|
||||
<div class="srow idle"><span class="st idle"></span><span class="stitle">shell · npm test</span><span class="smeta">✓ idle</span><span class="x spush">✕</span></div>
|
||||
</div>
|
||||
<div class="actions"><button class="btn new">+ New claude</button><button class="btn ghost">▾</button></div>
|
||||
</div>
|
||||
<div class="card running">
|
||||
<div class="crow"><span class="name">colacoder-api</span><span class="countbadge"><span class="d"></span>1 session</span></div>
|
||||
<div class="meta"><span class="chip">⎇ feat/auth</span><span class="chip">5d ago</span></div>
|
||||
<div class="path">~/Documents/git/colacoder-api</div>
|
||||
<div class="sessions"><div class="srow idle"><span class="st idle"></span><span class="stitle">claude</span><span class="smeta">✓ idle · 20m</span><span class="x spush">✕</span></div></div>
|
||||
<div class="actions"><button class="btn new">+ New claude</button><button class="btn ghost">▾</button></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="star">★</span><span class="name">dotfiles</span><span class="badge idle">idle</span></div>
|
||||
<div class="meta"><span class="chip">⎇ main</span><span class="chip">12d ago</span></div>
|
||||
<div class="path">~/.dotfiles</div>
|
||||
<div class="thumb empty"><span class="plus">+ start claude here</span></div>
|
||||
<div class="actions"><button class="btn">Open ▸ claude</button><button class="btn ghost">▾</button></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="crow"><span class="name">scratch</span><span class="badge idle">idle</span></div>
|
||||
<div class="meta"><span class="chip" style="opacity:.6">not a git repo</span></div>
|
||||
<div class="path">~/scratch</div>
|
||||
<div class="thumb empty"><span class="plus">+ start shell here</span></div>
|
||||
<div class="actions"><button class="btn shell">Open ▸ shell</button><button class="btn ghost">▾</button></div>
|
||||
</div>
|
||||
<div class="card add"><div class="big">+</div><div>Add folder…</div><div style="font-size:11px;opacity:.7">browse a path to track</div></div>
|
||||
</div>
|
||||
<div class="note">UI mockup · 一项目可挂多 session(每行可单独进入 / ✕Kill)· working=强调色 waiting=琥珀 idle=中性灰</div>
|
||||
</div>
|
||||
<script>
|
||||
const NAMES={amber:'Amber 琥珀金',sand:'Sand 沙褐',clay:'Clay 陶土',brass:'Brass 黄铜',slate:'Slate 冷石灰'};
|
||||
const p=(location.hash||'#amber').slice(1);
|
||||
document.body.dataset.p=p;
|
||||
document.getElementById('palname').textContent=NAMES[p]||p;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
270
docs/mockups/project-manager.html
Normal file
270
docs/mockups/project-manager.html
Normal file
@@ -0,0 +1,270 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Project Manager — UI Mockup</title>
|
||||
<style>
|
||||
:root{
|
||||
/* warm near-neutral dark base — no blue/purple */
|
||||
--bg:#100f0d; --panel:#181613; --card:#1b1916;
|
||||
--border:rgba(255,255,255,.07); --border-strong:rgba(255,255,255,.14);
|
||||
--text:#ece9e3; --text-dim:#a8a299; --text-faint:#6f6a61;
|
||||
/* emerald accent */
|
||||
--accent:#3ecf8e; --accent-2:#2bb377;
|
||||
--accent-soft:rgba(62,207,142,.14); --accent-line:rgba(62,207,142,.35);
|
||||
/* status */
|
||||
--working:#3ecf8e; --waiting:#f5b14c; --idle:#8b8a86; --dirty:#e8a13c; --red:#ff6b6b;
|
||||
--shadow:0 12px 44px rgba(0,0,0,.55); --radius:9px; --radius-lg:14px;
|
||||
--ui-font:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:
|
||||
radial-gradient(1100px 520px at 82% -12%, rgba(62,207,142,.10), transparent 62%),
|
||||
var(--bg);
|
||||
color:var(--text);font-family:var(--ui-font);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||||
|
||||
/* top bar */
|
||||
.topbar{height:50px;display:flex;align-items:center;gap:14px;padding:0 18px;
|
||||
border-bottom:1px solid var(--border);background:rgba(24,22,19,.62);
|
||||
backdrop-filter:blur(14px);position:sticky;top:0;z-index:5}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:650;letter-spacing:.2px}
|
||||
.brand .dot{width:11px;height:11px;border-radius:50%;
|
||||
background:conic-gradient(from 200deg,var(--accent),var(--accent-2),var(--accent))}
|
||||
.seg{display:flex;background:#0c0b09;border:1px solid var(--border);
|
||||
border-radius:999px;padding:3px;gap:2px;margin-left:6px}
|
||||
.seg button{all:unset;cursor:pointer;font-size:13px;color:var(--text-dim);
|
||||
padding:5px 15px;border-radius:999px;transition:.15s;display:flex;align-items:center;gap:6px}
|
||||
.seg button.active{background:var(--accent-soft);color:#bff3da;
|
||||
box-shadow:inset 0 0 0 1px var(--accent-line)}
|
||||
.seg .mini{font-size:10px;background:var(--accent);color:#08120c;border-radius:999px;
|
||||
padding:1px 6px;font-weight:700}
|
||||
.spacer{flex:1}
|
||||
.icns{display:flex;gap:6px}
|
||||
.icns button{all:unset;cursor:pointer;width:33px;height:33px;border-radius:8px;
|
||||
display:grid;place-items:center;color:var(--text-dim);font-size:15px;transition:.15s}
|
||||
.icns button:hover{background:var(--accent-soft);color:var(--text)}
|
||||
|
||||
/* content */
|
||||
.wrap{max-width:1100px;margin:0 auto;padding:26px 22px 60px}
|
||||
.head{display:flex;align-items:baseline;gap:12px;margin:4px 0 22px}
|
||||
.head h1{font-size:20px;margin:0;font-weight:680;letter-spacing:.2px}
|
||||
.head .sub{color:var(--text-faint);font-size:13px}
|
||||
.head .grow{flex:1}
|
||||
.search{display:flex;align-items:center;gap:8px;background:var(--panel);
|
||||
border:1px solid var(--border);border-radius:999px;padding:8px 15px;width:260px;
|
||||
color:var(--text-faint);font-size:13px;transition:.15s}
|
||||
.search:focus-within{border-color:var(--accent-line)}
|
||||
.search input{all:unset;flex:1;color:var(--text);font-size:13px}
|
||||
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:16px}
|
||||
|
||||
.card{background:linear-gradient(180deg,var(--card),#161410);
|
||||
border:1px solid var(--border);border-radius:var(--radius-lg);
|
||||
padding:15px;display:flex;flex-direction:column;gap:11px;
|
||||
box-shadow:var(--shadow);transition:.18s;cursor:default;position:relative;overflow:hidden}
|
||||
.card:hover{border-color:var(--border-strong);transform:translateY(-2px)}
|
||||
.card.running{border-color:var(--accent-line)}
|
||||
.card.running::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.8}
|
||||
|
||||
.crow{display:flex;align-items:center;gap:8px;min-width:0}
|
||||
.name{font-weight:680;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.star{color:var(--waiting);font-size:13px}
|
||||
.badge{margin-left:auto;font-size:11px;padding:3px 10px;border-radius:999px;
|
||||
display:flex;align-items:center;gap:6px;white-space:nowrap}
|
||||
.badge.idle{background:rgba(255,255,255,.05);color:var(--text-faint)}
|
||||
.countbadge{margin-left:auto;font-size:11px;padding:3px 10px;border-radius:999px;
|
||||
background:var(--accent-soft);color:#bff3da;display:flex;align-items:center;gap:6px;font-weight:600}
|
||||
.countbadge .d{width:6px;height:6px;border-radius:50%;background:var(--accent);
|
||||
box-shadow:0 0 9px var(--accent)}
|
||||
|
||||
.meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
||||
.chip{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--border);
|
||||
color:var(--text-dim);display:flex;align-items:center;gap:5px;font-family:var(--mono)}
|
||||
.chip.dirty{color:var(--dirty);border-color:rgba(232,161,60,.32)}
|
||||
.chip .gd{width:6px;height:6px;border-radius:50%;background:var(--dirty)}
|
||||
.path{font-family:var(--mono);font-size:11px;color:var(--text-faint);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-2px}
|
||||
|
||||
/* session rows (1 project : N sessions) */
|
||||
.sessions{display:flex;flex-direction:column;gap:7px}
|
||||
.srow{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:10px;
|
||||
background:rgba(255,255,255,.025);border:1px solid var(--border);cursor:pointer;
|
||||
transition:.15s;position:relative}
|
||||
.srow::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:2px;border-radius:2px;
|
||||
background:var(--rail,transparent)}
|
||||
.srow:hover{background:var(--accent-soft);border-color:var(--accent-line)}
|
||||
.srow.working{--rail:var(--working)} .srow.waiting{--rail:var(--waiting)} .srow.idle{--rail:var(--idle)}
|
||||
.srow .st{width:8px;height:8px;border-radius:50%;flex:0 0 auto;margin-left:3px}
|
||||
.st.working{background:var(--working);box-shadow:0 0 9px var(--working)}
|
||||
.st.waiting{background:var(--waiting);box-shadow:0 0 9px var(--waiting);animation:blink 1.1s steps(2) infinite}
|
||||
.st.idle{background:var(--idle)}
|
||||
@keyframes blink{50%{opacity:.25}}
|
||||
.srow .stitle{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.srow .smeta{font-size:11px;color:var(--text-faint);font-family:var(--mono);white-space:nowrap}
|
||||
.srow .mir{font-size:10px;color:var(--text-dim);background:rgba(255,255,255,.06);
|
||||
padding:2px 7px;border-radius:999px;white-space:nowrap}
|
||||
.srow .x{color:var(--text-faint);font-size:13px;padding:2px 5px;border-radius:6px;transition:.12s}
|
||||
.srow .x:hover{color:var(--red);background:rgba(255,107,107,.13)}
|
||||
.spush{margin-left:auto}
|
||||
|
||||
/* empty project preview */
|
||||
.thumb.empty{height:96px;border-radius:10px;border:1px dashed var(--border-strong);
|
||||
background:rgba(255,255,255,.015);display:grid;place-items:center;color:var(--text-faint)}
|
||||
.thumb.empty .plus{font-size:13px;color:var(--accent);
|
||||
border:1px dashed var(--accent-line);padding:7px 16px;border-radius:999px}
|
||||
|
||||
.actions{display:flex;gap:8px;margin-top:2px}
|
||||
.btn{all:unset;cursor:pointer;flex:1;text-align:center;font-size:12px;font-weight:680;
|
||||
padding:9px;border-radius:10px;background:var(--accent);color:#08120c;transition:.15s}
|
||||
.btn:hover{filter:brightness(1.07)}
|
||||
.btn.new{background:var(--accent-soft);color:#bff3da;box-shadow:inset 0 0 0 1px var(--accent-line)}
|
||||
.btn.shell{background:rgba(255,255,255,.07);color:var(--text)}
|
||||
.btn.ghost{flex:0 0 auto;padding:9px 13px;background:transparent;color:var(--text-dim);
|
||||
border:1px solid var(--border)}
|
||||
.btn.ghost:hover{border-color:var(--border-strong);color:var(--text)}
|
||||
|
||||
.card.add{border-style:dashed;align-items:center;justify-content:center;
|
||||
color:var(--text-faint);min-height:236px;gap:6px;cursor:pointer}
|
||||
.card.add:hover{color:var(--text);border-color:var(--accent-line)}
|
||||
.card.add .big{font-size:30px;color:var(--accent)}
|
||||
.note{margin-top:28px;color:var(--text-faint);font-size:12px;text-align:center;line-height:1.7}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<div class="brand"><span class="dot"></span> web-terminal</div>
|
||||
<div class="seg">
|
||||
<button>Sessions</button>
|
||||
<button class="active">Projects <span class="mini">2</span></button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="icns"><button>⚙</button><button>🗂</button><button>⌨</button></div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<h1>Projects</h1>
|
||||
<span class="sub">5 found · 2 running · 4 sessions</span>
|
||||
<span class="grow"></span>
|
||||
<div class="search">🔍 <input placeholder="search projects…"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<!-- project with MULTIPLE sessions (1:N) -->
|
||||
<div class="card running">
|
||||
<div class="crow">
|
||||
<span class="star">★</span>
|
||||
<span class="name">web-terminal</span>
|
||||
<span class="countbadge"><span class="d"></span>3 sessions</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="chip">⎇ main</span>
|
||||
<span class="chip dirty"><span class="gd"></span>dirty</span>
|
||||
<span class="chip">2h ago</span>
|
||||
</div>
|
||||
<div class="path">~/Documents/git/web-terminal</div>
|
||||
<div class="sessions">
|
||||
<div class="srow working">
|
||||
<span class="st working"></span>
|
||||
<span class="stitle">claude</span>
|
||||
<span class="smeta">⚙ working</span>
|
||||
<span class="mir spush">👥 2</span>
|
||||
<span class="x">✕</span>
|
||||
</div>
|
||||
<div class="srow waiting">
|
||||
<span class="st waiting"></span>
|
||||
<span class="stitle">claude #2</span>
|
||||
<span class="smeta">⏳ waiting approval</span>
|
||||
<span class="x spush">✕</span>
|
||||
</div>
|
||||
<div class="srow idle">
|
||||
<span class="st idle"></span>
|
||||
<span class="stitle">shell · npm test</span>
|
||||
<span class="smeta">✓ idle</span>
|
||||
<span class="x spush">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn new">+ New claude</button>
|
||||
<button class="btn ghost">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- project with ONE session -->
|
||||
<div class="card running">
|
||||
<div class="crow">
|
||||
<span class="name">colacoder-api</span>
|
||||
<span class="countbadge"><span class="d"></span>1 session</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="chip">⎇ feat/auth</span>
|
||||
<span class="chip">5d ago</span>
|
||||
</div>
|
||||
<div class="path">~/Documents/git/colacoder-api</div>
|
||||
<div class="sessions">
|
||||
<div class="srow idle">
|
||||
<span class="st idle"></span>
|
||||
<span class="stitle">claude</span>
|
||||
<span class="smeta">✓ idle · 20m</span>
|
||||
<span class="x spush">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn new">+ New claude</button>
|
||||
<button class="btn ghost">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- idle project (0 sessions) -->
|
||||
<div class="card">
|
||||
<div class="crow">
|
||||
<span class="star">★</span>
|
||||
<span class="name">dotfiles</span>
|
||||
<span class="badge idle">idle</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="chip">⎇ main</span>
|
||||
<span class="chip">12d ago</span>
|
||||
</div>
|
||||
<div class="path">~/.dotfiles</div>
|
||||
<div class="thumb empty"><span class="plus">+ start claude here</span></div>
|
||||
<div class="actions">
|
||||
<button class="btn">Open ▸ claude</button>
|
||||
<button class="btn ghost">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- non-git folder (0 sessions) -->
|
||||
<div class="card">
|
||||
<div class="crow">
|
||||
<span class="name">scratch</span>
|
||||
<span class="badge idle">idle</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="chip" style="opacity:.6">not a git repo</span>
|
||||
</div>
|
||||
<div class="path">~/scratch</div>
|
||||
<div class="thumb empty"><span class="plus">+ start shell here</span></div>
|
||||
<div class="actions">
|
||||
<button class="btn shell">Open ▸ shell</button>
|
||||
<button class="btn ghost">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- add folder -->
|
||||
<div class="card add">
|
||||
<div class="big">+</div>
|
||||
<div>Add folder…</div>
|
||||
<div style="font-size:11px;opacity:.7">browse a path to track</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="note">UI mockup · 暖灰底 + 翡翠绿强调色(无蓝/紫)· 状态色:working 绿 / waiting 琥珀 / idle 中性灰 · 一项目可挂多 session,每行可单独进入或 ✕Kill。</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/mockups/project-manager.png
Normal file
BIN
docs/mockups/project-manager.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
Reference in New Issue
Block a user