diff --git a/CHANGELOG.md b/CHANGELOG.md index d922c5e..6b242d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format follows the spirit of [Keep a Changelog](https://keepachangelog.com/e - Added project provenance, contribution authorship guidance, full-tree formatting, and type-aware ESLint parsing. - Removed fundraising, commercial, design-partner, community-operations, and enterprise-roadmap material from the public tree and npm package. - Added a release workflow for signed annotated tags, exact-tarball smoke testing, checksums, SBOM generation, build attestations, npm provenance, and post-publish `gitHead` verification. +- Centered the README hero and dashboard product identity, and added responsive navigation and request/detail layouts for mobile screens. ## 0.1.8 - 2026-08-22 diff --git a/README.md b/README.md index 92092e0..ded2abd 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# GhostAPI - -**Local API simulation and test evidence for AI-assisted development.** - -GhostAPI gives applications and coding agents a local target for selected Stripe, Resend, OpenAI, Twilio, GitHub, Discord, and generic REST workflows. It records heuristically sanitized traffic, injects deterministic failures, exposes MCP controls, and turns captured behavior into repeatable tests. - -On supported Linux hosts, `ghostapi run` can execute a test command inside a loopback-only network namespace. It is not a hostile-code or filesystem sandbox. - -

+

+ GhostAPI logo +

GhostAPI

+

Local API simulation and test evidence for AI-assisted development.

+

GhostAPI gives applications and coding agents a local target for selected Stripe, Resend, OpenAI, Twilio, GitHub, Discord, and generic REST workflows. It records heuristically sanitized traffic, injects deterministic failures, exposes MCP controls, and turns captured behavior into repeatable tests.

+

On supported Linux hosts, ghostapi run can execute a test command inside a loopback-only network namespace. It is not a hostile-code or filesystem sandbox.

+

npm license ci node MCP -

- -[Quickstart](#quickstart) · [Security model](SECURITY.md) · [Provider coverage](docs/providers/) · [Release evidence](docs/releases/) - -```bash -npx @yiaany/ghostapi start --open -``` +

+

+ Quickstart · + Security model · + Provider coverage · + Release evidence +

+

npx @yiaany/ghostapi start --open

+

GhostAPI dashboard with live local Stripe, OpenAI, and REST traffic diff --git a/docs/assets/dashboard.png b/docs/assets/dashboard.png index 71ab5e9..634a346 100644 Binary files a/docs/assets/dashboard.png and b/docs/assets/dashboard.png differ diff --git a/src/dashboard/app.js b/src/dashboard/app.js index 409f27a..2d78ff5 100644 --- a/src/dashboard/app.js +++ b/src/dashboard/app.js @@ -1,6 +1,7 @@ // DOM References const els = { sidebar: document.getElementById("sidebar"), + sidebarBackdrop: document.getElementById("sidebar-backdrop"), toggleBtn: document.getElementById("sidebar-toggle"), statusIndicator: document.getElementById("status-indicator"), statusText: document.getElementById("status-text"), @@ -82,7 +83,25 @@ async function fetchProviderManifests() { // Side-effects & Listeners function setupInteractions() { - els.toggleBtn.onclick = () => els.sidebar.classList.toggle("closed"); + els.toggleBtn.onclick = () => { + if (window.matchMedia("(max-width: 760px)").matches) { + const open = els.sidebar.classList.toggle("mobile-open"); + els.sidebarBackdrop.classList.toggle("open", open); + return; + } + els.sidebar.classList.toggle("closed"); + }; + els.sidebarBackdrop.onclick = closeMobileSidebar; + els.sidebar.addEventListener("click", (event) => { + if (event.target.closest(".nav-item")) closeMobileSidebar(); + }); + window.addEventListener("resize", () => { + if (window.matchMedia("(max-width: 760px)").matches) { + els.sidebar.classList.remove("closed"); + return; + } + closeMobileSidebar(); + }); // Command + K document.addEventListener("keydown", (e) => { @@ -183,8 +202,14 @@ window.setFilter = (filter) => { } renderList(); + closeMobileSidebar(); }; +function closeMobileSidebar() { + els.sidebar.classList.remove("mobile-open"); + els.sidebarBackdrop.classList.remove("open"); +} + window.clearStorage = async (target, event) => { try { const btnText = event.currentTarget.querySelector(".nav-item-title"); diff --git a/src/dashboard/index.html b/src/dashboard/index.html index dbbf965..1d5c207 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -400,12 +400,25 @@ + +

- - +
+
diff --git a/src/dashboard/styles.css b/src/dashboard/styles.css index e7415c4..95b344a 100644 --- a/src/dashboard/styles.css +++ b/src/dashboard/styles.css @@ -76,6 +76,15 @@ body { opacity: 0; border-right: none; } +.sidebar-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 30; + border: 0; + background: rgba(0, 0, 0, 0.58); + backdrop-filter: blur(2px); +} /* Workspace Switcher */ .ws-switcher { @@ -292,9 +301,9 @@ body { .topbar { height: 56px; border-bottom: 1px solid var(--border); - display: flex; + display: grid; + grid-template-columns: minmax(44px, 1fr) auto minmax(44px, 1fr); align-items: center; - justify-content: space-between; padding: 0 16px; background: var(--card); flex-shrink: 0; @@ -303,6 +312,8 @@ body { display: flex; align-items: center; gap: 12px; + grid-column: 1; + justify-self: start; } .toggle-btn { padding: 6px; @@ -326,6 +337,35 @@ body { font-size: 14px; color: var(--muted-foreground); } +.topbar-center { + position: fixed; + top: 0; + left: 50%; + z-index: 5; + height: 56px; + transform: translateX(-50%); + min-width: 0; +} +.brand-lockup { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--foreground); +} +.brand-lockup strong { + font-size: 14px; + font-weight: 650; + letter-spacing: -0.01em; +} +.topbar-brand-icon { + width: 22px; + height: 22px; + border-radius: 6px; + object-fit: cover; +} +.breadcrumb-divider { + color: rgba(161, 161, 170, 0.42); +} .breadcrumb-active { font-weight: 500; color: var(--foreground); @@ -335,6 +375,8 @@ body { display: flex; align-items: center; gap: 12px; + grid-column: 3; + justify-self: end; } .status-indicator { display: flex; @@ -820,6 +862,129 @@ body { } } +@media (max-width: 760px) { + .sidebar { + position: fixed; + inset: 0 auto 0 0; + z-index: 40; + width: min(84vw, 300px); + transform: translateX(-100%); + opacity: 0; + pointer-events: none; + border-right: 1px solid rgba(255, 255, 255, 0.14); + background: #0d0d10; + box-shadow: 24px 0 64px rgba(0, 0, 0, 0.42); + transition: + transform 0.22s ease, + opacity 0.22s ease; + } + .sidebar.closed { + width: min(84vw, 300px); + padding: 12px; + opacity: 0; + border-right: 1px solid rgba(255, 255, 255, 0.14); + } + .sidebar.mobile-open { + transform: translateX(0); + opacity: 1; + pointer-events: auto; + } + .sidebar-backdrop.open { + display: block; + } + .topbar { + height: 52px; + padding: 0 12px; + grid-template-columns: 36px minmax(0, 1fr) 36px; + } + .topbar-center { + height: 52px; + max-width: 100%; + gap: 6px; + } + .breadcrumb-active { + max-width: 32vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .status-indicator { + padding: 5px; + border-radius: 50%; + } + #status-text { + display: none; + } + .content-wrapper { + flex-direction: column; + } + .event-list-pane { + width: 100%; + height: 42%; + min-height: 210px; + border-right: none; + border-bottom: 1px solid var(--border); + } + .details-pane { + min-height: 0; + } + .dep-header { + align-items: flex-start; + flex-direction: column; + gap: 12px; + padding: 12px 16px; + } + .dep-title { + width: 100%; + overflow: hidden; + } + .dep-title > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dep-badges { + width: 100%; + flex-wrap: wrap; + } + .json-split { + flex-direction: column; + overflow: auto; + } + .json-pane { + min-height: 230px; + border-right: none; + border-bottom: 1px solid var(--border); + } + .json-pane:last-child { + border-bottom: none; + } + .rules-modal-header { + flex-wrap: wrap; + } +} + +@media (max-width: 420px) { + .brand-lockup strong { + font-size: 13px; + } + .topbar-brand-icon { + width: 19px; + height: 19px; + } + .breadcrumb-divider, + .breadcrumb-active { + font-size: 12px; + } + .event-list-pane { + height: 46%; + min-height: 190px; + } + .event-card { + padding: 10px 12px; + } +} + @media (max-height: 720px) { .setup-modal-content { height: calc(100vh - 48px); diff --git a/test/dashboardAssets.test.ts b/test/dashboardAssets.test.ts index 4893666..49629a6 100644 --- a/test/dashboardAssets.test.ts +++ b/test/dashboardAssets.test.ts @@ -19,6 +19,7 @@ describe("dashboard assets", () => { expect(app).toContain("els.eventCount.textContent = list.length"); expect(app).toContain('scenarioButton("Arm"'); expect(app).not.toContain('scenarioButton("Replay"'); + expect(app).toContain("closeMobileSidebar"); }); it("does not load dashboard assets from external origins", async () => { @@ -34,4 +35,23 @@ describe("dashboard assets", () => { expect(content).not.toContain("@import url("); } }); + + it("keeps the product identity centered and provides a mobile layout", async () => { + const [html, styles, readme] = await Promise.all([ + readFile("src/dashboard/index.html", "utf8"), + readFile("src/dashboard/styles.css", "utf8"), + readFile("README.md", "utf8"), + ]); + + expect(html).toContain('class="breadcrumb topbar-center"'); + expect(html).toContain('id="sidebar-backdrop"'); + expect(styles).toContain("grid-template-columns: minmax(44px, 1fr) auto"); + expect(styles).toContain("grid-column: 3"); + expect(styles).toContain("transform: translateX(-50%)"); + expect(styles).toContain("@media (max-width: 760px)"); + expect(styles).toContain(".sidebar.mobile-open"); + expect(styles).toContain("flex-direction: column"); + expect(readme).toContain('
'); + expect(readme).toContain('Quickstart'); + }); });