启动跨平台桌面客户端项目
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
parent
3a982c8f22
commit
1748d2d66a
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
src-tauri/target
|
||||||
|
*.log
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
src-tauri/target/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22.18.0-alpine3.22 AS dependencies
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM dependencies AS test
|
||||||
|
COPY . .
|
||||||
|
CMD ["npm", "test"]
|
||||||
|
|
||||||
|
FROM dependencies AS build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginxinc/nginx-unprivileged:1.29.1-alpine3.22 AS preview
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 8080
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1
|
||||||
42
README.md
42
README.md
@ -1,2 +1,42 @@
|
|||||||
# gaza
|
# Gaza
|
||||||
|
|
||||||
|
Gaza 是一个协议优先、跨平台的多 Teamserver 桌面工作台。当前仓库包含可运行的首个项目骨架,重点验证 Teamserver 工作区和标签页隔离模型。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Tauri 2(桌面容器)
|
||||||
|
- React 19 + TypeScript + Vite(界面)
|
||||||
|
- Vitest + Testing Library(单元与组件测试)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
本机安装 Node.js 22 与 Rust stable 后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run dev # 浏览器开发模式
|
||||||
|
npm run tauri dev # 桌面开发模式
|
||||||
|
```
|
||||||
|
|
||||||
|
无需本机 Node.js 的前端验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm test
|
||||||
|
docker compose run --rm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前范围
|
||||||
|
|
||||||
|
- 展示多 Teamserver 连接状态和独立工作区。
|
||||||
|
- 切换 Teamserver 时隔离标签、项目导航和激活状态。
|
||||||
|
- 所有演示业务状态仅保存在进程内存,不写入本地存储。
|
||||||
|
- 提供 Tauri 最小权限配置;后续连接、协议与加密 SQLite 能力按独立迭代接入。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
npm run tauri build
|
||||||
|
```
|
||||||
|
|||||||
26
compose.yaml
Normal file
26
compose.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: test
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
build:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: build
|
||||||
|
preview:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: preview
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /var/cache/nginx
|
||||||
|
- /var/run
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#151515" />
|
||||||
|
<title>Gaza</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3271
package-lock.json
generated
Normal file
3271
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "gaza",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"typecheck": "tsc -b --pretty false",
|
||||||
|
"test": "vitest run",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "2.8.0",
|
||||||
|
"react": "19.1.1",
|
||||||
|
"react-dom": "19.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "2.8.4",
|
||||||
|
"@testing-library/jest-dom": "6.8.0",
|
||||||
|
"@testing-library/react": "16.3.0",
|
||||||
|
"@types/react": "19.1.10",
|
||||||
|
"@types/react-dom": "19.1.7",
|
||||||
|
"@vitejs/plugin-react": "5.0.2",
|
||||||
|
"jsdom": "26.1.0",
|
||||||
|
"typescript": "5.9.2",
|
||||||
|
"vite": "7.3.6",
|
||||||
|
"vitest": "3.2.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src-tauri/Cargo.toml
Normal file
18
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "gaza"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "多 Teamserver 桌面工作台"
|
||||||
|
authors = ["Gaza contributors"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "gaza_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.4.1", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.8.5", features = [] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
7
src-tauri/capabilities/default.json
Normal file
7
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "主窗口的最小权限",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
6
src-tauri/src/lib.rs
Normal file
6
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("启动 Gaza 桌面客户端失败");
|
||||||
|
}
|
||||||
3
src-tauri/src/main.rs
Normal file
3
src-tauri/src/main.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
gaza_lib::run();
|
||||||
|
}
|
||||||
17
src-tauri/tauri.conf.json
Normal file
17
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Gaza",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "red.gaza.desktop",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [{ "title": "Gaza", "width": 1280, "height": 800, "minWidth": 900, "minHeight": 600 }],
|
||||||
|
"security": { "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" }
|
||||||
|
},
|
||||||
|
"bundle": { "active": true, "targets": "all" }
|
||||||
|
}
|
||||||
16
src/App.test.tsx
Normal file
16
src/App.test.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
describe("Teamserver 工作区", () => {
|
||||||
|
it("切换服务器时只显示对应服务器标签,并恢复各自激活标签", () => {
|
||||||
|
render(<App />);
|
||||||
|
expect(screen.getByRole("tab", { name: /Session Overview/ })).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /Session Overview/ }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "切换到 Ember Lab" }));
|
||||||
|
expect(screen.queryByRole("tab", { name: /Session Overview/ })).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("tab", { name: /Task Output/ })).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "切换到 Atlas Teamserver" }));
|
||||||
|
expect(screen.getByRole("tab", { name: /Session Overview/ })).toHaveAttribute("aria-selected", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
91
src/App.tsx
Normal file
91
src/App.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { initialWorkspaces, selectWorkspace } from "./workspace";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [activeServerId, setActiveServerId] = useState(initialWorkspaces[0].id);
|
||||||
|
const [activeTabs, setActiveTabs] = useState<Record<string, string>>(
|
||||||
|
Object.fromEntries(initialWorkspaces.map((server) => [server.id, server.activeTabId])),
|
||||||
|
);
|
||||||
|
const server = useMemo(
|
||||||
|
() => selectWorkspace(initialWorkspaces, activeServerId) ?? initialWorkspaces[0],
|
||||||
|
[activeServerId],
|
||||||
|
);
|
||||||
|
const activeTab = server.tabs.find((tab) => tab.id === activeTabs[server.id]) ?? server.tabs[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="shell">
|
||||||
|
<aside className="server-rail" aria-label="Teamserver 列表">
|
||||||
|
<div className="brand">G</div>
|
||||||
|
{initialWorkspaces.map((item) => (
|
||||||
|
<button
|
||||||
|
className={`server-button ${item.id === server.id ? "active" : ""}`}
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setActiveServerId(item.id)}
|
||||||
|
aria-label={`切换到 ${item.name}`}
|
||||||
|
title={item.name}
|
||||||
|
>
|
||||||
|
{item.shortName}
|
||||||
|
<span className={`presence ${item.connection.toLowerCase()}`} />
|
||||||
|
{item.unread > 0 && <span className="badge">{item.unread}</span>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button className="server-button add" aria-label="添加 Teamserver">+</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside className="project-sidebar">
|
||||||
|
<header>
|
||||||
|
<p className="eyebrow">TEAMSERVER</p>
|
||||||
|
<h1>{server.name}</h1>
|
||||||
|
<span className={`connection ${server.connection.toLowerCase()}`}>{server.connection}</span>
|
||||||
|
</header>
|
||||||
|
<label className="search">
|
||||||
|
<span>⌕</span>
|
||||||
|
<input aria-label="搜索" placeholder="搜索" />
|
||||||
|
<kbd>⌘ K</kbd>
|
||||||
|
</label>
|
||||||
|
<nav aria-label="项目导航">
|
||||||
|
<a>收件箱 <strong>{server.unread}</strong></a>
|
||||||
|
<p className="nav-heading">项目 · {server.activeProject}</p>
|
||||||
|
{['Overview', 'Sessions', 'Tasks', 'Events', 'Artifacts', 'Audit'].map((item) => (
|
||||||
|
<a className={item === activeTab.title ? "selected" : ""} key={item}>{item}</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<p className="memory-note">仅内存状态 · 退出后清除</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="workspace">
|
||||||
|
<div className="tabbar" role="tablist" aria-label={`${server.name} 标签`}>
|
||||||
|
{server.tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
role="tab"
|
||||||
|
aria-selected={tab.id === activeTab.id}
|
||||||
|
className={tab.id === activeTab.id ? "active" : ""}
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTabs((tabs) => ({ ...tabs, [server.id]: tab.id }))}
|
||||||
|
title={`${tab.project} / ${tab.session}`}
|
||||||
|
>
|
||||||
|
{tab.running && <span className="running" />}{tab.title}<span className="close">×</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="contextbar">
|
||||||
|
{server.name} <span>/</span> {activeTab.project} <span>/</span> {activeTab.session} <span>/</span> {activeTab.title}
|
||||||
|
</div>
|
||||||
|
<article className="content">
|
||||||
|
<div className="content-heading">
|
||||||
|
<div><p className="eyebrow">{activeTab.project}</p><h2>{activeTab.title}</h2></div>
|
||||||
|
<button className="primary">新建操作</button>
|
||||||
|
</div>
|
||||||
|
<div className="notice"><span>✓</span><div><strong>工作区上下文已锁定</strong><p>此标签固定绑定到 {server.name} / {activeTab.project} / {activeTab.session}。</p></div></div>
|
||||||
|
<div className="stats">
|
||||||
|
<section><p>连接状态</p><strong>{server.connection}</strong></section>
|
||||||
|
<section><p>打开标签</p><strong>{server.tabs.length}</strong></section>
|
||||||
|
<section><p>未读事件</p><strong>{server.unread}</strong></section>
|
||||||
|
</div>
|
||||||
|
<section className="empty-state"><span>⌁</span><h3>协议工作区已就绪</h3><p>连接标准 Teamserver 后,Capability 将驱动此处的操作和结果视图。</p></section>
|
||||||
|
</article>
|
||||||
|
<footer className="statusbar"><span>● {server.connection}</span><span>身份:operator@example</span><span>协议:等待协商</span><span>事件流:演示模式</span></footer>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
56
src/styles.css
Normal file
56
src/styles.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
:root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; color: #292724; background: #f7f6f3; font-synthesis: none; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; min-width: 900px; min-height: 100vh; }
|
||||||
|
button, input { font: inherit; }
|
||||||
|
button { color: inherit; }
|
||||||
|
.shell { display: grid; grid-template-columns: 56px 250px 1fr; min-height: 100vh; }
|
||||||
|
.server-rail { background: #181817; padding: 12px 8px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
||||||
|
.brand { width: 36px; height: 36px; display: grid; place-items: center; color: #111; background: #f1e65d; border-radius: 10px; font-weight: 900; margin-bottom: 8px; }
|
||||||
|
.server-button { position: relative; width: 38px; height: 38px; border: 0; border-radius: 10px; background: #353432; color: #d9d7d2; font-size: 11px; cursor: pointer; }
|
||||||
|
.server-button:hover, .server-button.active { background: #ece9df; color: #171716; }
|
||||||
|
.server-button.active::before { content: ""; position: absolute; width: 3px; height: 24px; background: #f1e65d; left: -8px; top: 7px; border-radius: 2px; }
|
||||||
|
.server-button.add { color: #aaa; font-size: 22px; background: transparent; border: 1px dashed #555; }
|
||||||
|
.presence { position: absolute; width: 9px; height: 9px; border: 2px solid #181817; border-radius: 50%; right: -2px; bottom: -2px; background: #47b881; }
|
||||||
|
.presence.degraded { background: #e8a441; }
|
||||||
|
.badge { position: absolute; min-width: 16px; height: 16px; padding: 0 4px; line-height: 16px; background: #d85b53; color: white; border-radius: 9px; right: -7px; top: -7px; font-size: 9px; }
|
||||||
|
.project-sidebar { background: #eeece7; border-right: 1px solid #dedbd4; display: flex; flex-direction: column; padding: 20px 14px 10px; }
|
||||||
|
.project-sidebar header { padding: 0 6px 14px; }
|
||||||
|
.eyebrow { color: #8b8881; font-size: 10px; font-weight: 700; letter-spacing: .12em; margin: 0 0 5px; }
|
||||||
|
h1 { font-size: 16px; margin: 0 0 8px; }
|
||||||
|
.connection { font-size: 9px; font-weight: 800; color: #36865d; background: #dbeadd; border-radius: 4px; padding: 3px 6px; }
|
||||||
|
.connection.degraded { color: #9b6b21; background: #f3e4c9; }
|
||||||
|
.search { display: flex; align-items: center; gap: 7px; margin: 8px 4px 18px; padding: 7px 8px; border: 1px solid #d5d2cb; border-radius: 6px; background: #f8f7f4; color: #7c7973; }
|
||||||
|
.search input { min-width: 0; width: 100%; border: 0; outline: 0; background: transparent; }
|
||||||
|
kbd { font-size: 9px; white-space: nowrap; }
|
||||||
|
nav { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
nav a { display: flex; justify-content: space-between; padding: 7px 10px; border-radius: 5px; color: #5e5b56; font-size: 13px; cursor: default; }
|
||||||
|
nav a.selected { background: #dedbd4; color: #242321; font-weight: 600; }
|
||||||
|
nav strong { color: #9b554e; }
|
||||||
|
.nav-heading { margin: 20px 10px 5px; font-size: 10px; color: #85817b; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
|
||||||
|
.memory-note { margin-top: auto; padding: 9px; font-size: 10px; color: #8a8780; }
|
||||||
|
.workspace { display: grid; grid-template-rows: 39px 43px 1fr 24px; min-width: 0; height: 100vh; }
|
||||||
|
.tabbar { display: flex; background: #e7e5e0; border-bottom: 1px solid #d6d3cc; overflow: hidden; }
|
||||||
|
.tabbar button { max-width: 190px; border: 0; border-right: 1px solid #d7d4cd; background: transparent; padding: 0 11px; font-size: 11px; color: #74716c; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.tabbar button.active { background: #f7f6f3; color: #292724; }
|
||||||
|
.close { color: #aaa; margin-left: 15px; }
|
||||||
|
.running { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #e0a53b; margin-right: 7px; }
|
||||||
|
.contextbar { border-bottom: 1px solid #e4e1da; padding: 13px 20px; font-size: 11px; color: #77736d; }
|
||||||
|
.contextbar span { color: #bbb7af; padding: 0 7px; }
|
||||||
|
.content { overflow: auto; padding: 40px max(40px, 7vw); }
|
||||||
|
.content-heading { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e1ded7; padding-bottom: 22px; }
|
||||||
|
h2 { font-family: Georgia, serif; font-size: 30px; margin: 0; font-weight: 500; }
|
||||||
|
.primary { border: 0; background: #272624; color: #fff; padding: 8px 13px; border-radius: 5px; font-size: 12px; }
|
||||||
|
.notice { display: flex; gap: 12px; margin: 22px 0; padding: 14px; background: #edf3ec; border: 1px solid #d9e7d6; border-radius: 7px; font-size: 12px; }
|
||||||
|
.notice > span { color: #4e8e5f; }
|
||||||
|
.notice p { color: #686d66; margin: 4px 0 0; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
.stats section { background: #fff; border: 1px solid #e3e0d9; border-radius: 7px; padding: 15px; }
|
||||||
|
.stats p { margin: 0 0 9px; color: #8a8780; font-size: 11px; }
|
||||||
|
.stats strong { font-size: 14px; }
|
||||||
|
.empty-state { text-align: center; padding: 70px 20px; color: #8a8780; }
|
||||||
|
.empty-state > span { font-size: 38px; color: #c3bfb7; }
|
||||||
|
.empty-state h3 { color: #4d4a46; font-family: Georgia, serif; font-weight: 500; margin-bottom: 8px; }
|
||||||
|
.empty-state p { font-size: 12px; }
|
||||||
|
.statusbar { display: flex; gap: 24px; align-items: center; padding: 0 12px; color: #aaa69e; background: #262523; font-size: 9px; }
|
||||||
|
.statusbar span:first-child { color: #70bd91; }
|
||||||
|
@media (max-width: 1000px) { .shell { grid-template-columns: 52px 220px 1fr; } .content { padding: 30px; } }
|
||||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
57
src/workspace.ts
Normal file
57
src/workspace.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
export type ConnectionState = "CONNECTED" | "DEGRADED" | "RECONNECTING";
|
||||||
|
|
||||||
|
export interface OperationTab {
|
||||||
|
id: string;
|
||||||
|
project: string;
|
||||||
|
session: string;
|
||||||
|
title: string;
|
||||||
|
running?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamserverWorkspace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
shortName: string;
|
||||||
|
connection: ConnectionState;
|
||||||
|
unread: number;
|
||||||
|
activeProject: string;
|
||||||
|
activeTabId: string;
|
||||||
|
tabs: OperationTab[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialWorkspaces: TeamserverWorkspace[] = [
|
||||||
|
{
|
||||||
|
id: "atlas",
|
||||||
|
name: "Atlas Teamserver",
|
||||||
|
shortName: "AT",
|
||||||
|
connection: "CONNECTED",
|
||||||
|
unread: 2,
|
||||||
|
activeProject: "Northwind",
|
||||||
|
activeTabId: "nw-sessions",
|
||||||
|
tabs: [
|
||||||
|
{ id: "nw-sessions", project: "Northwind", session: "All sessions", title: "Sessions" },
|
||||||
|
{ id: "nw-042", project: "Northwind", session: "NW-042", title: "Session Overview" },
|
||||||
|
{ id: "or-audit", project: "Orchid", session: "Project", title: "Audit", running: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ember",
|
||||||
|
name: "Ember Lab",
|
||||||
|
shortName: "EL",
|
||||||
|
connection: "DEGRADED",
|
||||||
|
unread: 5,
|
||||||
|
activeProject: "Sandbox",
|
||||||
|
activeTabId: "sb-events",
|
||||||
|
tabs: [
|
||||||
|
{ id: "sb-events", project: "Sandbox", session: "Project", title: "Events" },
|
||||||
|
{ id: "sb-107", project: "Sandbox", session: "SB-107", title: "Task Output", running: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function selectWorkspace(
|
||||||
|
workspaces: TeamserverWorkspace[],
|
||||||
|
workspaceId: string,
|
||||||
|
): TeamserverWorkspace | undefined {
|
||||||
|
return workspaces.find(({ id }) => id === workspaceId);
|
||||||
|
}
|
||||||
20
tsconfig.app.json
Normal file
20
tsconfig.app.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
clearScreen: false,
|
||||||
|
server: { port: 1420, strictPort: true },
|
||||||
|
envPrefix: ["VITE_", "TAURI_ENV_"],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "./src/test/setup.ts",
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user