diff --git a/src/App.test.tsx b/src/App.test.tsx index 13fda6a..50aa59e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -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(); + 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(); + }); }); diff --git a/src/App.tsx b/src/App.tsx index fb47a37..7925cb3 100644 --- a/src/App.tsx +++ b/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>( @@ -68,23 +75,33 @@ export function App() { ))} -
- {server.name} / {activeTab.project} / {activeTab.session} / {activeTab.title} +
+ ◆ LOCKED{server.name} / {activeTab.project} / {activeTab.session} / {activeTab.title}
-

{activeTab.project}

{activeTab.title}

- +

{activeTab.title}

{activeTab.project} 中的 248 个 Session · 4 个实时更新

+
-
工作区上下文已锁定

此标签固定绑定到 {server.name} / {activeTab.project} / {activeTab.session}。

-
-

连接状态

{server.connection}
-

打开标签

{server.tabs.length}
-

未读事件

{server.unread}
+
+ + +
-

协议工作区已就绪

连接标准 Teamserver 后,Capability 将驱动此处的操作和结果视图。

+
+
STATUS ↕SESSION IDHOST / USEROSSOURCE ADDRESSLAST SEEN ↓TAGS
+ {sessions.map((session) =>
+ + {session.status} + {session.id} + {session.host}{session.user} + {session.os}{session.address}{session.seen} + {session.tags.map((tag) => {tag})} +
)} +
已选择 0 项1–50 / 248
+
-
● {server.connection}身份:operator@example协议:等待协商事件流:演示模式
+ ); diff --git a/src/styles.css b/src/styles.css index 7a94407..f471353 100644 --- a/src/styles.css +++ b/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; } } diff --git a/src/test/setup.ts b/src/test/setup.ts index f149f27..bb32bff 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1 +1,5 @@ import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; + +afterEach(cleanup);