统一专业作战工作台视觉设计
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
parent
70f69b0683
commit
1a32791922
@ -13,4 +13,12 @@ describe("Teamserver 工作区", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "切换到 Atlas Teamserver" }));
|
||||
expect(screen.getByRole("tab", { name: /Session Overview/ })).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
|
||||
it("显示锁定上下文和高密度 Session 表格", () => {
|
||||
render(<App />);
|
||||
expect(screen.getByLabelText("锁定的操作上下文")).toHaveTextContent("Atlas Teamserver / Northwind / All sessions / Sessions");
|
||||
expect(screen.getByLabelText("Session 表格")).toBeInTheDocument();
|
||||
expect(screen.getByText("NW-042")).toBeInTheDocument();
|
||||
expect(screen.getByText("10.42.8.17")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
39
src/App.tsx
39
src/App.tsx
@ -1,6 +1,13 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { initialWorkspaces, selectWorkspace } from "./workspace";
|
||||
|
||||
const sessions = [
|
||||
{ id: "NW-042", status: "ONLINE", host: "ws-fin-042", user: "nora.chen", os: "Windows 11", address: "10.42.8.17", seen: "14s ago", tags: ["finance", "priority"] },
|
||||
{ id: "NW-038", status: "ONLINE", host: "srv-app-03", user: "svc.deploy", os: "Ubuntu 24.04", address: "10.42.3.8", seen: "51s ago", tags: ["server"] },
|
||||
{ id: "NW-031", status: "IDLE", host: "mac-design-07", user: "alex.k", os: "macOS 15.6", address: "10.42.12.27", seen: "8m ago", tags: ["design"] },
|
||||
{ id: "NW-019", status: "OFFLINE", host: "ws-ops-019", user: "operator", os: "Windows 10", address: "10.42.6.91", seen: "2h ago", tags: ["legacy"] },
|
||||
];
|
||||
|
||||
export function App() {
|
||||
const [activeServerId, setActiveServerId] = useState(initialWorkspaces[0].id);
|
||||
const [activeTabs, setActiveTabs] = useState<Record<string, string>>(
|
||||
@ -68,23 +75,33 @@ export function App() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="contextbar">
|
||||
{server.name} <span>/</span> {activeTab.project} <span>/</span> {activeTab.session} <span>/</span> {activeTab.title}
|
||||
<div className="contextbar" aria-label="锁定的操作上下文">
|
||||
<span className="context-lock">◆ LOCKED</span>{server.name} <i>/</i> {activeTab.project} <i>/</i> <b>{activeTab.session}</b> <i>/</i> {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><h2>{activeTab.title}</h2><p>{activeTab.project} 中的 248 个 Session · 4 个实时更新</p></div>
|
||||
<div className="heading-actions"><button className="secondary">导出视图</button><button className="primary">+ 新建操作</button></div>
|
||||
</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 className="table-toolbar">
|
||||
<label><span>⌕</span><input aria-label="筛选 Session" placeholder="筛选 Session…" /></label>
|
||||
<button>状态:全部⌄</button><button>标签⌄</button><button>+ 筛选</button>
|
||||
<span className="toolbar-spacer" /><button aria-label="调整列">☷ 列</button><button aria-label="更多操作">•••</button>
|
||||
</div>
|
||||
<section className="empty-state"><span>⌁</span><h3>协议工作区已就绪</h3><p>连接标准 Teamserver 后,Capability 将驱动此处的操作和结果视图。</p></section>
|
||||
<section className="session-table" aria-label="Session 表格">
|
||||
<div className="table-row table-head" role="row"><span><input type="checkbox" aria-label="选择全部" /></span><span>STATUS ↕</span><span>SESSION ID</span><span>HOST / USER</span><span>OS</span><span>SOURCE ADDRESS</span><span>LAST SEEN ↓</span><span>TAGS</span><span /></div>
|
||||
{sessions.map((session) => <div className={`table-row ${session.id === "NW-042" ? "focused" : ""}`} role="row" key={session.id} tabIndex={0}>
|
||||
<span><input type="checkbox" aria-label={`选择 ${session.id}`} /></span>
|
||||
<span><em className={`status-mark ${session.status.toLowerCase()}`} />{session.status}</span>
|
||||
<strong className="mono">{session.id}</strong>
|
||||
<span><b>{session.host}</b><small>{session.user}</small></span>
|
||||
<span>{session.os}</span><span className="mono muted">{session.address}</span><span className="mono muted">{session.seen}</span>
|
||||
<span className="tags">{session.tags.map((tag) => <small key={tag}>{tag}</small>)}</span><button className="row-action" aria-label={`${session.id} 快速操作`}>•••</button>
|
||||
</div>)}
|
||||
<footer><span>已选择 0 项</span><span>1–50 / 248</span><button>‹</button><button>›</button></footer>
|
||||
</section>
|
||||
</article>
|
||||
<footer className="statusbar"><span>● {server.connection}</span><span>身份:operator@example</span><span>协议:等待协商</span><span>事件流:演示模式</span></footer>
|
||||
<footer className="statusbar"><span>● {server.connection}</span><span>身份 operator@example</span><span>延迟 38ms</span><span>协议 v1.0</span><span>事件流 ✓ 已同步</span><span className="status-spacer" />内存工作区</footer>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
142
src/styles.css
142
src/styles.css
@ -1,56 +1,90 @@
|
||||
:root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; color: #292724; background: #f7f6f3; font-synthesis: none; }
|
||||
:root {
|
||||
font-family: Inter, "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #e7eaf0; background: #0f1115; font-synthesis: none; font-size: 13px;
|
||||
--app: #0f1115; --nav: #14171c; --content: #181c22; --hover: #20252d;
|
||||
--selected: #27313d; --border: #2a3039; --text: #e7eaf0; --muted: #929aa8;
|
||||
--online: #38b57a; --warning: #d9a441; --error: #e05b63; --info: #4c8dda;
|
||||
}
|
||||
* { 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; }
|
||||
body { margin: 0; min-width: 900px; min-height: 100vh; background: var(--app); }
|
||||
button, input { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; }
|
||||
button:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--info); outline-offset: -2px; }
|
||||
.shell { display: grid; grid-template-columns: 52px 260px 1fr; min-height: 100vh; background: var(--app); }
|
||||
.server-rail { background: #101318; border-right: 1px solid var(--border); padding: 10px 7px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.brand { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid #3a414c; border-radius: 5px; color: var(--text); background: #1b2027; font: 700 14px ui-monospace, "JetBrains Mono", monospace; margin-bottom: 9px; }
|
||||
.server-button { position: relative; width: 36px; height: 36px; border: 1px solid transparent; border-radius: 5px; background: #1b2027; color: #a9b0bc; font: 600 10px ui-monospace, "JetBrains Mono", monospace; transition: background 100ms ease, border-color 100ms ease; }
|
||||
.server-button:hover { background: var(--hover); border-color: #343c47; color: var(--text); }
|
||||
.server-button.active { background: var(--selected); border-color: #3c4959; color: var(--text); }
|
||||
.server-button.active::before { content: ""; position: absolute; width: 2px; height: 22px; background: var(--info); left: -8px; top: 6px; border-radius: 1px; }
|
||||
.server-button.add { color: var(--muted); font-size: 18px; background: transparent; border: 1px dashed #3a414c; }
|
||||
.presence { position: absolute; width: 8px; height: 8px; border: 2px solid #101318; border-radius: 50%; right: -3px; bottom: -3px; background: var(--online); }
|
||||
.presence.degraded { background: var(--warning); }
|
||||
.badge { position: absolute; min-width: 16px; height: 16px; padding: 0 4px; line-height: 16px; background: var(--error); color: white; border-radius: 8px; right: -7px; top: -7px; font: 600 9px ui-monospace, monospace; }
|
||||
.project-sidebar { background: var(--nav); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 18px 12px 9px; }
|
||||
.project-sidebar header { padding: 0 7px 13px; }
|
||||
.eyebrow { color: var(--muted); font-size: 10px; font-weight: 650; letter-spacing: .1em; margin: 0 0 6px; }
|
||||
h1 { font-size: 14px; line-height: 20px; margin: 0 0 8px; font-weight: 600; }
|
||||
.connection { display: inline-flex; font: 600 9px ui-monospace, monospace; color: #82d5aa; background: #193128; border: 1px solid #28523e; border-radius: 4px; padding: 2px 5px; }
|
||||
.connection::before { content: "✓"; margin-right: 4px; }
|
||||
.connection.degraded { color: #e5bb67; background: #322a1d; border-color: #5a4727; }
|
||||
.connection.degraded::before { content: "!"; }
|
||||
.search, .table-toolbar label { display: flex; align-items: center; gap: 7px; border: 1px solid var(--border); border-radius: 4px; background: #111419; color: var(--muted); }
|
||||
.search { margin: 8px 1px 17px; padding: 6px 8px; }
|
||||
.search input, .table-toolbar input { min-width: 0; width: 100%; border: 0; outline: 0; background: transparent; }
|
||||
kbd { border: 1px solid #343b45; border-radius: 3px; padding: 1px 4px; 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; } }
|
||||
nav a { display: flex; justify-content: space-between; padding: 6px 9px; border-radius: 4px; color: #aab1bd; font-size: 12px; transition: background 100ms ease; }
|
||||
nav a:hover { background: var(--hover); }
|
||||
nav a.selected { background: var(--selected); color: var(--text); font-weight: 550; box-shadow: inset 2px 0 var(--info); }
|
||||
nav strong { color: #f1888f; font: 600 10px ui-monospace, monospace; }
|
||||
.nav-heading { margin: 18px 9px 5px; font-size: 10px; color: #737c89; font-weight: 650; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.memory-note { margin-top: auto; padding: 9px; border-top: 1px solid var(--border); font-size: 10px; color: #727b88; }
|
||||
.workspace { display: grid; grid-template-rows: 38px 42px 1fr 24px; min-width: 0; height: 100vh; background: var(--content); }
|
||||
.tabbar { display: flex; background: #111419; border-bottom: 1px solid var(--border); overflow: hidden; }
|
||||
.tabbar button { min-width: 132px; max-width: 190px; border: 0; border-right: 1px solid var(--border); background: transparent; padding: 0 11px; font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background 100ms ease; }
|
||||
.tabbar button:hover { background: var(--hover); color: #cbd0d8; }
|
||||
.tabbar button.active { background: var(--content); color: var(--text); box-shadow: inset 0 2px var(--info); }
|
||||
.close { color: #69717d; margin-left: 15px; }
|
||||
.running { display: inline-block; width: 6px; height: 6px; border-radius: 2px; background: var(--warning); margin-right: 7px; }
|
||||
.contextbar { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--border); padding: 0 16px; font-size: 11px; color: var(--muted); background: #171b21; }
|
||||
.contextbar i { color: #515967; font-style: normal; }
|
||||
.contextbar b { color: #cdd2da; font: 500 11px ui-monospace, "JetBrains Mono", monospace; }
|
||||
.context-lock { color: #77a9e4; font: 650 9px ui-monospace, monospace; border-right: 1px solid var(--border); padding-right: 10px; margin-right: 2px; }
|
||||
.content { overflow: auto; padding: 22px 24px 30px; background: var(--content); }
|
||||
.content-heading { display: flex; align-items: center; justify-content: space-between; padding-bottom: 18px; }
|
||||
.content-heading h2 { font-size: 20px; line-height: 27px; margin: 0; font-weight: 600; letter-spacing: -.01em; }
|
||||
.content-heading p { margin: 3px 0 0; color: var(--muted); font-size: 11px; }
|
||||
.heading-actions { display: flex; gap: 7px; }
|
||||
.primary, .secondary, .table-toolbar button { border: 1px solid var(--border); border-radius: 4px; padding: 6px 9px; font-size: 11px; background: #1b2027; }
|
||||
.primary { border-color: #457dbd; background: #326ba9; color: #fff; }
|
||||
.primary:hover { background: #3d78b8; }
|
||||
.secondary:hover, .table-toolbar button:hover { background: var(--hover); }
|
||||
.table-toolbar { height: 40px; display: flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-bottom: 0; padding: 5px 7px; background: #15191f; }
|
||||
.table-toolbar label { width: 220px; height: 28px; padding: 0 8px; }
|
||||
.table-toolbar button { height: 28px; padding: 0 8px; color: #aeb5c0; }
|
||||
.toolbar-spacer, .status-spacer { flex: 1; }
|
||||
.session-table { min-width: 880px; border: 1px solid var(--border); background: #15191f; }
|
||||
.table-row { display: grid; grid-template-columns: 34px 92px 104px minmax(150px, 1.3fr) 120px 130px 92px minmax(120px, 1fr) 34px; min-height: 36px; border-bottom: 1px solid #252b33; align-items: stretch; font-size: 12px; color: #cbd0d8; }
|
||||
.table-row > span, .table-row > strong, .table-row > button { display: flex; align-items: center; padding: 0 9px; min-width: 0; border-right: 1px solid #222831; }
|
||||
.table-row:hover { background: var(--hover); }
|
||||
.table-row.focused { background: #202a35; box-shadow: inset 2px 0 var(--info); }
|
||||
.table-head { position: sticky; top: -22px; z-index: 2; min-height: 30px; background: #111419; color: #78818e; font-size: 9px; font-weight: 650; letter-spacing: .04em; }
|
||||
.table-row b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 550; color: #dde1e7; }
|
||||
.table-row small { display: block; color: #7f8895; font-size: 10px; }
|
||||
.mono { font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; font-weight: 500; color: #9ec7f4; }
|
||||
.muted { color: var(--muted); }
|
||||
.status-mark { width: 7px; height: 7px; margin-right: 7px; border-radius: 2px; background: var(--online); }
|
||||
.status-mark.idle { background: var(--warning); }
|
||||
.status-mark.offline { background: #69717d; box-shadow: inset 0 0 0 1px #89919c; }
|
||||
.tags { gap: 4px; }
|
||||
.tags small { display: inline; padding: 2px 5px; border: 1px solid #343c47; border-radius: 3px; background: #1d2229; color: #a8b0bb; }
|
||||
.row-action { border: 0; background: transparent; color: #79818d; }
|
||||
.session-table > footer { display: flex; align-items: center; justify-content: flex-end; gap: 10px; height: 34px; padding: 0 8px; color: #7f8895; font-size: 10px; }
|
||||
.session-table > footer span:first-child { margin-right: auto; }
|
||||
.session-table > footer button { width: 24px; height: 22px; border: 1px solid var(--border); background: #1b2027; border-radius: 3px; color: var(--muted); }
|
||||
.statusbar { display: flex; gap: 20px; align-items: center; padding: 0 10px; border-top: 1px solid #2b323c; color: #78818d; background: #111419; font-size: 9px; }
|
||||
.statusbar span:first-child { color: #75c99c; }
|
||||
input[type="checkbox"] { accent-color: var(--info); width: 12px; height: 12px; }
|
||||
@media (max-width: 1050px) { .shell { grid-template-columns: 52px 230px 1fr; } .content { padding-inline: 16px; } .table-row { grid-template-columns: 34px 84px 96px minmax(140px, 1fr) 110px 120px 84px minmax(110px, 1fr) 34px; } }
|
||||
@media (prefers-reduced-motion: reduce) { * { transition-duration: 0.01ms !important; } }
|
||||
|
||||
@ -1 +1,5 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user