diff --git a/apps/web/public/brand/npm-wordmark.svg b/apps/web/public/brand/npm-wordmark.svg new file mode 100644 index 00000000..79102c27 --- /dev/null +++ b/apps/web/public/brand/npm-wordmark.svg @@ -0,0 +1,12 @@ + + diff --git a/apps/web/src/components/agent-setup-view/agent-setup-data.ts b/apps/web/src/components/agent-setup-view/agent-setup-data.ts index fe452142..0f62cd67 100644 --- a/apps/web/src/components/agent-setup-view/agent-setup-data.ts +++ b/apps/web/src/components/agent-setup-view/agent-setup-data.ts @@ -53,13 +53,9 @@ export const agentSetupEntries: readonly AgentSetupEntry[] = [ icon: "/agents/codex.svg", id: "codex", name: "Codex", - notes: [ - "Installs as a Codex plugin, so the skill is always a command away.", - "No account, API key, or local browser needed.", - ], + notes: ["No account, API key, or local browser needed."], status: "Plugin", - summary: - "Install the plugin, then ask Codex to diagram anything.", + summary: "Install the plugin, then ask Codex to diagram anything.", tagline: "OpenAI's coding agent", }, { @@ -84,13 +80,9 @@ curl -fsSL ${portableSkillUrl} -o "$SKETCHI_SKILL_DIR/SKILL.md"`, icon: "/agents/opencode.svg", id: "opencode", name: "OpenCode", - notes: [ - "Setup is just the skill plus the public MCP server.", - "No account, API key, or local browser needed; provider and model behavior remain OpenCode concerns.", - ], + notes: ["No account, API key, or local browser needed."], status: "MCP", - summary: - "Point OpenCode at Sketchi and diagram from the terminal.", + summary: "Point OpenCode at Sketchi and diagram from the terminal.", tagline: "Open-source terminal agent", }, { @@ -120,13 +112,9 @@ curl -fsSL ${portableSkillUrl} -o "$SKETCHI_SKILL_DIR/SKILL.md"`, icon: "/agents/claude-code.svg", id: "claude-code", name: "Claude Code", - notes: [ - "The plugin bundles the skill, so you can ask in plain language.", - "No account, API key, or local browser needed.", - ], + notes: ["No account, API key, or local browser needed."], status: "Plugin", - summary: - "Add the plugin, then turn any explanation into a diagram.", + summary: "Add the plugin, then turn any explanation into a diagram.", tagline: "Anthropic's coding agent", }, { @@ -152,12 +140,10 @@ curl -fsSL ${portableSkillUrl} -o .agents/skills/sketchi-code-mode/SKILL.md`, name: "Antigravity", notes: [ "Save as .agents/mcp_config.json, merging the sketchi-code-mode server into any existing mcpServers object instead of overwriting it.", - "Setup is just the skill plus the public MCP server.", - "No account, API key, or local browser needed; model and harness behavior remain Agy concerns.", + "No account, API key, or local browser needed.", ], status: "MCP", - summary: - "Add the skill and point Agy at the Code Mode server.", + summary: "Add the skill and point Agy at the Code Mode server.", tagline: "Google's agent IDE", }, ] satisfies readonly AgentSetupEntry[]; diff --git a/apps/web/src/components/agent-setup-view/agent-setup-view.test.tsx b/apps/web/src/components/agent-setup-view/agent-setup-view.test.tsx index 59b2d62c..286b09a0 100644 --- a/apps/web/src/components/agent-setup-view/agent-setup-view.test.tsx +++ b/apps/web/src/components/agent-setup-view/agent-setup-view.test.tsx @@ -20,9 +20,7 @@ describe("AgentSetupView", () => { "href", "http://localhost:3000/agents/opencode", ); - expect( - screen.getByText("https://playground.sketchi.app/mcp"), - ).toBeTruthy(); + expect(screen.getByText("https://playground.sketchi.app/mcp")).toBeTruthy(); expect(screen.queryByRole("link", { name: /Eval harness/ })).toBeNull(); expect(screen.queryByRole("link", { name: /Excalidraw app/ })).toBeNull(); }); @@ -41,7 +39,9 @@ describe("AgentSetupView", () => { expect( screen.getByText(/opencode\/skills\/sketchi-code-mode/), ).toBeTruthy(); - expect(screen.getByText(/provider and model behavior/)).toBeTruthy(); + expect( + screen.getByText("No account, API key, or local browser needed."), + ).toBeTruthy(); }); it("renders the Codex plugin marketplace commands", () => { diff --git a/apps/web/src/components/agent-setup-view/agent-setup-view.tsx b/apps/web/src/components/agent-setup-view/agent-setup-view.tsx index 85f79dbf..d2d85331 100644 --- a/apps/web/src/components/agent-setup-view/agent-setup-view.tsx +++ b/apps/web/src/components/agent-setup-view/agent-setup-view.tsx @@ -22,12 +22,11 @@ export function AgentSetupView({ agentId }: AgentSetupViewProps) { return ; } -function EndpointCard({ meta }: { meta: string }) { +function EndpointCard() { return (
Server URL {codeModeMcpEndpoint} - {meta}
); } @@ -41,10 +40,10 @@ function AgentSetupHub() {

Sketch diagrams without leaving your agent.

Connect it once, then ask in plain language. Your agent hands back - a real, editable diagram, not a wall of ASCII. + a real, editable diagram.

- + @@ -101,7 +100,7 @@ function AgentSetupDetail({ entry }: { entry: AgentSetupEntry }) {

{entry.summary}

- + diff --git a/apps/web/src/components/cli-band/cli-band.test.tsx b/apps/web/src/components/cli-band/cli-band.test.tsx index 8f1e3ddf..1d19a243 100644 --- a/apps/web/src/components/cli-band/cli-band.test.tsx +++ b/apps/web/src/components/cli-band/cli-band.test.tsx @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest"; import { CliBand } from "./cli-band"; import { CLI_INSTALL_COMMAND, + CLI_NODE_REQUIREMENT, CLI_NPM_INSTALL_COMMAND, CLI_NPM_URL, } from "../../lib/cli-package"; @@ -60,20 +61,29 @@ describe("CliBand", () => { }); /** - * `generate`, `share`, and `pull` all make HTTPS requests. Only the other - * seven commands are offline, so a blanket "offline except generation" claim - * is false — and a wrong privacy claim is worse than a vague one. + * Generation needs the network, so counting the commands that do not is a + * claim nobody installs a CLI for. The band makes no offline claim at all + * now, and must not grow one back. */ - it("does not overstate what works offline", () => { + it("makes no offline claim", () => { + const { container } = render(); + + expect(container.textContent).not.toMatch(/offline/i); + }); + + /** + * Every install command carried a sentence explaining it. The commands are + * self-evident, so the terminal card is commands and the Node floor, nothing + * else. + */ + it("captions none of the commands", () => { const { container } = render(); - const text = container.textContent ?? ""; - expect(text).not.toMatch(/offline for everything but generation/); expect( - screen.getByText( - "Create, patch, show, edit, list, export, and restore never touch the network or a model.", + [...container.querySelectorAll(".cli-band__body p")].map( + (element) => element.textContent, ), - ).toBeTruthy(); + ).toEqual([`Requires ${CLI_NODE_REQUIREMENT}.`]); }); it("links to the real npm package page with the npm mark", () => { @@ -88,14 +98,13 @@ describe("CliBand", () => { ); }); - it("links to the CLI docs section", () => { - render(); + it("offers npm as the section's only trailing link", () => { + const { container } = render(); + expect(container.querySelectorAll(".cli-band__links a")).toHaveLength(1); expect( - screen - .getByRole("link", { name: /Read the CLI docs/ }) - .getAttribute("href"), - ).toBe("/docs#cli"); + screen.queryByRole("link", { name: /Read the CLI docs/ }), + ).toBeNull(); }); it("is anchored so navigation and footer entries can reach it", () => { diff --git a/apps/web/src/components/cli-band/cli-band.tsx b/apps/web/src/components/cli-band/cli-band.tsx index c667cfb5..cd85b96e 100644 --- a/apps/web/src/components/cli-band/cli-band.tsx +++ b/apps/web/src/components/cli-band/cli-band.tsx @@ -5,15 +5,12 @@ import { CLI_NODE_REQUIREMENT, CLI_NPM_URL, CLI_PACKAGE_NAME, - cliCapabilities, cliInstallOptions, } from "../../lib/cli-package"; export interface CliBandProps { /** Link to the published npm package. */ npmUrl?: string; - /** Where the fuller CLI writeup lives. */ - docsHref?: string; } /** @@ -21,10 +18,7 @@ export interface CliBandProps { * on this page; the terminal is a peer of both, so it gets one too — the * install command is the section, copy-pasteable, not a link to go find it. */ -export function CliBand({ - docsHref = "/docs#cli", - npmUrl = CLI_NPM_URL, -}: CliBandProps) { +export function CliBand({ npmUrl = CLI_NPM_URL }: CliBandProps) { return (
@@ -33,30 +27,16 @@ export function CliBand({ Or never leave the terminal.

- {CLI_PACKAGE_NAME} is a real command-line tool, not a - wrapper around this website. It keeps your diagrams in{" "} - ~/.sketchi/diagrams, does its authoring, export, and - recovery work offline, and speaks --output json on - every command so an agent can drive it. + {CLI_PACKAGE_NAME} draws diagrams from your shell. No + account, no API key, and --output json on every command + so an agent can drive it.

-
- {cliCapabilities.map((capability) => ( -
-
{capability.title}
-
{capability.description}
-
- ))} -
-
@@ -72,19 +52,15 @@ export function CliBand({
{cliInstallOptions.map((option) => ( -
-
-
-                    {option.command}
-                  
- -
-

{option.detail}

+
+
+                  {option.command}
+                
+
))} -

Then ask for a diagram:

-
+
                 {CLI_EXAMPLE_COMMAND}
               
diff --git a/apps/web/src/components/cta-band/cta-band.test.tsx b/apps/web/src/components/cta-band/cta-band.test.tsx index 9f828865..9697cfb7 100644 --- a/apps/web/src/components/cta-band/cta-band.test.tsx +++ b/apps/web/src/components/cta-band/cta-band.test.tsx @@ -20,13 +20,15 @@ describe("CtaBand", () => { ).toHaveProperty("href", "http://localhost:3000/agents"); }); - it("offers the CLI as the third closing route", () => { - render(); + /** + * Three trailing routes out of one closing sheet is clutter. The playground + * is the primary, the agent hub is the one alternative, and the CLI keeps + * its own full section further up the page. + */ + it("closes on one primary action and one alternative", () => { + const { container } = render(); - expect( - screen - .getByRole("link", { name: /install the CLI/ }) - .getAttribute("href"), - ).toBe("/#cli"); + expect(container.querySelectorAll(".cta-band__actions a")).toHaveLength(2); + expect(screen.queryByRole("link", { name: /install the CLI/ })).toBeNull(); }); }); diff --git a/apps/web/src/components/cta-band/cta-band.tsx b/apps/web/src/components/cta-band/cta-band.tsx index 158ff20c..22fdf5cb 100644 --- a/apps/web/src/components/cta-band/cta-band.tsx +++ b/apps/web/src/components/cta-band/cta-band.tsx @@ -5,19 +5,16 @@ export interface CtaBandProps { playgroundHref?: string; /** Link to the agent setup hub. */ agentsHref?: string; - /** Link to the CLI section. */ - cliHref?: string; } /** * Closing invitation — a warm, blank sketch sheet echoing the hero board. - * One clear next step, with the agent and terminal paths as quiet secondary - * links for readers the browser is not the answer for. + * One clear next step, and one quiet alternative for readers the browser is + * not the answer for. The CLI has its own section above. */ export function CtaBand({ playgroundHref = DEFAULT_WEB_SURFACE_URLS.playground, agentsHref = "/agents", - cliHref = "/#cli", }: CtaBandProps) { return (
@@ -33,9 +30,6 @@ export function CtaBand({ or add it to your coding agent → - - or install the CLI → -
diff --git a/apps/web/src/components/docs-view/docs-view.tsx b/apps/web/src/components/docs-view/docs-view.tsx index 26042944..2f2492e6 100644 --- a/apps/web/src/components/docs-view/docs-view.tsx +++ b/apps/web/src/components/docs-view/docs-view.tsx @@ -62,10 +62,9 @@ export function DocsView({

Three paths

- You describe it. Sketchi structures and validates the request before - drawing, so the result stays consistent, not a best-guess sketch. - What differs between the three paths is where that happens and who - keeps the file. + Sketchi structures and validates every request before drawing. What + differs between the three paths is where that happens and who keeps + the file.

@@ -129,20 +128,16 @@ export function DocsView({ {CLI_PACKAGE_NAME} is published on npm and authors diagrams from your shell. Records live under{" "} ~/.sketchi/diagrams with the canonical document, the - Excalidraw artifact, and every prior revision, so nothing you make - is trapped in a browser tab. + Excalidraw artifact, and every prior revision.

Install

{cliInstallOptions.map((option) => ( -
-
-
-                  {option.command}
-                
- -
-

{option.detail}

+
+
+                {option.command}
+              
+
))}

Requires {CLI_NODE_REQUIREMENT}.

@@ -154,9 +149,8 @@ export function DocsView({ generate
- Turn one prompt into a validated diagram. Makes a single - credential-free HTTPS request — no key, token, account, or - login. + Turn one prompt into a validated diagram. No key, token, + account, or login.
@@ -182,8 +176,8 @@ export function DocsView({ export
- Write PNG or Excalidraw. PNG rendering is deterministic and - local; it never starts a browser or uses the network. + Write PNG or Excalidraw. Local and deterministic; never uses the + network.
@@ -227,8 +221,7 @@ export function DocsView({

Open source

Sketchi is built in the open and MIT-licensed: the diagram engine, - the icon set, the agent runtime, even these pages. Read the code, - file an issue, or run your own copy. + the icon set, the agent runtime, even these pages.

Source @@ -278,7 +271,7 @@ export function DocsView({
Can I export?
-
Yes. Take it to your docs, slides, or pull request.
+
Yes, as PNG or Excalidraw.
Is there a CLI?
diff --git a/apps/web/src/components/feature-grid/feature-grid.test.tsx b/apps/web/src/components/feature-grid/feature-grid.test.tsx index 381b0925..498ce3a7 100644 --- a/apps/web/src/components/feature-grid/feature-grid.test.tsx +++ b/apps/web/src/components/feature-grid/feature-grid.test.tsx @@ -16,20 +16,15 @@ describe("FeatureGrid", () => { expect( screen.getByRole("heading", { name: "Yours to edit and export" }), ).toBeTruthy(); - expect( - screen.getByText( - "Every shape, connector, and label stays editable after generation.", - ), - ).toBeTruthy(); - expect( - screen.getByText( - "Find the tools in your stack without drawing their marks by hand.", - ), - ).toBeTruthy(); - expect( - screen.getByText( - "Open the scene in Excalidraw, then export it when you are ready.", - ), - ).toBeTruthy(); + }); + + /** + * The titles say the whole thing. A supporting sentence per card was padding + * dressed as polish, so the band carries its weight through layout instead. + */ + it("adds no restating sentence under a title", () => { + const { container } = render(); + + expect(container.querySelector(".feature-card p")).toBeNull(); }); }); diff --git a/apps/web/src/components/feature-grid/feature-grid.tsx b/apps/web/src/components/feature-grid/feature-grid.tsx index db62c0cb..100250c4 100644 --- a/apps/web/src/components/feature-grid/feature-grid.tsx +++ b/apps/web/src/components/feature-grid/feature-grid.tsx @@ -3,7 +3,6 @@ import type { ReactNode } from "react"; import { BrandIcon } from "../brand-icon/index.js"; interface Feature { - description: string; glyph: ReactNode; title: string; } @@ -17,14 +16,10 @@ const clusterIcons = [ const features: readonly Feature[] = [ { - description: - "Every shape, connector, and label stays editable after generation.", glyph: , title: "Real objects, not screenshots", }, { - description: - "Find the tools in your stack without drawing their marks by hand.", glyph: (
diff --git a/apps/web/src/components/site-footer/site-footer.test.tsx b/apps/web/src/components/site-footer/site-footer.test.tsx index b3f3db89..fafabd49 100644 --- a/apps/web/src/components/site-footer/site-footer.test.tsx +++ b/apps/web/src/components/site-footer/site-footer.test.tsx @@ -4,17 +4,12 @@ import { describe, expect, it } from "vitest"; import { SiteFooter } from "./site-footer"; describe("SiteFooter", () => { - it("renders the footer columns and colophon", () => { + it("renders the footer columns", () => { render(); expect(screen.getByRole("heading", { name: "Product" })).toBeTruthy(); expect(screen.getByRole("heading", { name: "Agents" })).toBeTruthy(); expect(screen.getByRole("heading", { name: "More" })).toBeTruthy(); - expect( - screen.getByText( - "Made for people who'd rather describe a diagram than draw one.", - ), - ).toBeTruthy(); expect( screen .getByRole("link", { name: "Sketchi on GitHub" }) @@ -22,6 +17,18 @@ describe("SiteFooter", () => { ).toBe("https://github.com/shpitdev/sketchi"); }); + /** + * The hero already states the thesis. The footer restated it twice more, in + * the brand blurb and the colophon; both are gone and neither comes back. + */ + it("does not restate the hero thesis", () => { + const { container } = render(); + const text = container.textContent ?? ""; + + expect(text).not.toMatch(/rather describe a diagram than draw one/); + expect(text).not.toMatch(/Prompts become real, editable diagrams/); + }); + it("links to llms.txt under its own name", () => { render(); @@ -39,20 +46,23 @@ describe("SiteFooter", () => { ).toBe("https://sketchi.app/llms.txt"); }); - it("lists the CLI as a product and links the npm package with its mark", () => { + /** + * The npm wordmark is the word, so the link carries the mark and an + * accessible name rather than the mark plus "npm package" beside it. + */ + it("lists the CLI as a product and links npm by its wordmark", () => { render(); expect(screen.getByRole("link", { name: "CLI" }).getAttribute("href")).toBe( "/#cli", ); - const npmLink = screen.getByRole("link", { name: /npm package/ }); + const npmLink = screen.getByRole("link", { name: "sketchi on npm" }); expect(npmLink.getAttribute("href")).toBe( "https://www.npmjs.com/package/sketchi", ); - expect(npmLink.querySelector("img")?.getAttribute("src")).toBe( - "/brand/npm.svg", - ); + expect(npmLink.querySelector(".npm-mark")).toBeTruthy(); + expect(screen.queryByText("npm package")).toBeNull(); }); it("uses configured surface links", () => { diff --git a/apps/web/src/components/site-footer/site-footer.tsx b/apps/web/src/components/site-footer/site-footer.tsx index af252808..fa95b646 100644 --- a/apps/web/src/components/site-footer/site-footer.tsx +++ b/apps/web/src/components/site-footer/site-footer.tsx @@ -1,4 +1,3 @@ -import { BrandIcon } from "../brand-icon/index.js"; import { CLI_NPM_URL } from "../../lib/cli-package"; import { DEFAULT_WEB_SURFACE_URLS, @@ -6,7 +5,6 @@ import { } from "../../lib/surface-urls"; export interface SiteFooterProps { - colophon?: string; // Every surface that renders this footer must actually serve the file it // points at; pass an absolute URL when the surface has no llms.txt of its own. llmsTxtUrl?: string; @@ -19,7 +17,6 @@ const DEFAULT_REPO_URL = "https://github.com/shpitdev/sketchi"; const DEFAULT_LLMS_TXT_URL = "/llms.txt"; export function SiteFooter({ - colophon = "Made for people who'd rather describe a diagram than draw one.", llmsTxtUrl = DEFAULT_LLMS_TXT_URL, npmUrl = CLI_NPM_URL, repoUrl = DEFAULT_REPO_URL, @@ -39,9 +36,6 @@ export function SiteFooter({ /> Sketchi -

- Prompts become real, editable diagrams, logos included. -

How it works
  • - - - npm package + +
  • @@ -117,7 +114,6 @@ export function SiteFooter({
    - {colophon} © Sketchi
    diff --git a/apps/web/src/components/site-header/site-header.test.tsx b/apps/web/src/components/site-header/site-header.test.tsx index 41bd9e6b..d1e7c18b 100644 --- a/apps/web/src/components/site-header/site-header.test.tsx +++ b/apps/web/src/components/site-header/site-header.test.tsx @@ -11,7 +11,7 @@ describe("SiteHeader", () => { expect(brand.getAttribute("href")).toBe("/"); expect(brand.querySelector(".site-header__brand-icon")).toBeTruthy(); expect(brand.querySelector(".sk-wordmark")?.textContent).toBe("Sketchi"); - expect(screen.getByRole("link", { name: "Product" })).toBeTruthy(); + expect(screen.queryByRole("link", { name: "Product" })).toBeNull(); expect(screen.getByRole("link", { name: "Agents" })).toBeTruthy(); expect( screen.getByRole("link", { name: "Playground" }).classList, @@ -21,6 +21,9 @@ describe("SiteHeader", () => { .getByRole("link", { name: "Sketchi on GitHub" }) .getAttribute("href"), ).toBe("https://github.com/shpitdev/sketchi"); + expect( + screen.getByRole("link", { name: "sketchi on npm" }).getAttribute("href"), + ).toBe("https://www.npmjs.com/package/sketchi"); expect( screen.getByRole("link", { name: "Docs" }).getAttribute("aria-current"), ).toBe("page"); @@ -65,6 +68,28 @@ describe("SiteHeader", () => { ).toEqual(["/#cli", "/#cli"]); }); + /** + * The header actions are hidden on narrow viewports, so both source marks + * have to survive into the sheet or they vanish on mobile entirely. + */ + it("carries the GitHub and npm marks into the mobile sheet", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: "Toggle menu" })); + + expect( + screen.getByRole("link", { name: "GitHub" }).getAttribute("href"), + ).toBe("https://github.com/shpitdev/sketchi"); + expect( + screen + .getAllByRole("link", { name: "sketchi on npm" }) + .map((link) => link.getAttribute("href")), + ).toEqual([ + "https://www.npmjs.com/package/sketchi", + "https://www.npmjs.com/package/sketchi", + ]); + }); + it("toggles the mobile menu", () => { render(); diff --git a/apps/web/src/components/site-header/site-header.tsx b/apps/web/src/components/site-header/site-header.tsx index e9c1c27b..76a17729 100644 --- a/apps/web/src/components/site-header/site-header.tsx +++ b/apps/web/src/components/site-header/site-header.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; +import { CLI_NPM_URL } from "../../lib/cli-package"; import { DEFAULT_WEB_SURFACE_URLS, type WebSurfaceUrls, @@ -13,12 +14,12 @@ export interface SiteHeaderNavItem { export interface SiteHeaderProps { activePath?: string; navItems?: readonly SiteHeaderNavItem[]; + npmUrl?: string; repoUrl?: string; surfaceUrls?: WebSurfaceUrls; } const defaultNavItems: readonly SiteHeaderNavItem[] = [ - { href: "/#product", label: "Product" }, { href: "/#cli", label: "CLI" }, { href: "/agents", label: "Agents" }, { href: "/docs", label: "Docs" }, @@ -29,6 +30,7 @@ const DEFAULT_REPO_URL = "https://github.com/shpitdev/sketchi"; export function SiteHeader({ activePath, navItems = defaultNavItems, + npmUrl = CLI_NPM_URL, repoUrl = DEFAULT_REPO_URL, surfaceUrls = DEFAULT_WEB_SURFACE_URLS, }: SiteHeaderProps) { @@ -67,13 +69,22 @@ export function SiteHeader({ + + + + Playground diff --git a/apps/web/src/lib/cli-package.ts b/apps/web/src/lib/cli-package.ts index 214c7d8f..6f170736 100644 --- a/apps/web/src/lib/cli-package.ts +++ b/apps/web/src/lib/cli-package.ts @@ -33,8 +33,6 @@ export const CLI_EXAMPLE_COMMAND = export interface CliInstallOption { /** The shell command itself. */ command: string; - /** What this path gets you, beyond the binary. */ - detail: string; /** Accessible label for the copy control. */ label: string; } @@ -42,42 +40,10 @@ export interface CliInstallOption { export const cliInstallOptions: readonly CliInstallOption[] = [ { command: CLI_INSTALL_COMMAND, - detail: - "Installs the package and sets up completions for zsh, bash, or fish.", label: "install script", }, { command: CLI_NPM_INSTALL_COMMAND, - detail: - "Straight from npm, if you would rather configure completions yourself.", label: "npm install command", }, ]; - -export interface CliCapability { - description: string; - title: string; -} - -/** - * Three honest capability claims. Each maps to real subcommands: the offline - * seven (`create`, `patch`, `show`, `edit`, `list`, `export`, `restore`), the - * credential-free `generate`, and the local PNG/Excalidraw exporter. - */ -export const cliCapabilities: readonly CliCapability[] = [ - { - description: - "One prompt, one HTTPS request, a validated diagram on disk. No API key, token, or account.", - title: "Generate without signing in", - }, - { - description: - "Create, patch, show, edit, list, export, and restore never touch the network or a model.", - title: "Seven commands work offline", - }, - { - description: - "Render PNG or Excalidraw locally, deterministically, without starting a browser.", - title: "Export from the terminal", - }, -]; diff --git a/apps/web/src/routes/-agents.test.tsx b/apps/web/src/routes/-agents.test.tsx index e366553c..bceb0939 100644 --- a/apps/web/src/routes/-agents.test.tsx +++ b/apps/web/src/routes/-agents.test.tsx @@ -47,9 +47,6 @@ describe("AgentDetailPage", () => { /curl -fsSL .*\.agents\/skills\/sketchi-code-mode\/SKILL\.md/, ), ).toBeTruthy(); - expect( - screen.getByText(/model and harness behavior remain Agy/), - ).toBeTruthy(); expect( screen.getByRole("heading", { name: "Save or merge .agents/mcp_config.json", diff --git a/apps/web/src/styles/app.css b/apps/web/src/styles/app.css index 64ebc497..c266c8f0 100644 --- a/apps/web/src/styles/app.css +++ b/apps/web/src/styles/app.css @@ -459,42 +459,69 @@ } /* ===================================================================== */ -/* GitHub mark — recolored via mask so it inherits ink on light, paper */ -/* on the dark footer. */ +/* Source marks — both recolored via mask so they inherit ink on light */ +/* and paper on the dark footer. npm's square tile does not survive that */ +/* treatment (its n is a knockout, so masking it leaves a bare frame), so */ +/* npm uses its wordmark, which is legible small and reads as the brand. */ +/* It keeps its natural 780:250 aspect rather than being squared off. */ /* ===================================================================== */ -.gh-mark { +.gh-mark, +.npm-mark { background: currentColor; - block-size: 20px; display: inline-block; flex: none; +} + +.gh-mark { + block-size: 20px; inline-size: 20px; mask: url("/brand/github.svg") center / contain no-repeat; -webkit-mask: url("/brand/github.svg") center / contain no-repeat; } -.site-header__gh { +.npm-mark { + block-size: 18px; + inline-size: 46px; + mask: url("/brand/npm-wordmark.svg") center / contain no-repeat; + -webkit-mask: url("/brand/npm-wordmark.svg") center / contain no-repeat; +} + +/* Footer link lists set their own type scale, so the mark tracks it. */ +.npm-mark--sm { + block-size: 15px; + inline-size: 38px; +} + +.site-header__mark { align-items: center; border-radius: var(--r-sm); color: var(--ink-2); display: inline-flex; block-size: 40px; - inline-size: 40px; justify-content: center; + min-inline-size: 40px; + padding-inline: 9px; transition: color 0.16s var(--ease), background 0.16s var(--ease); } -.site-header__gh:hover { +.site-header__mark:hover { background: color-mix(in srgb, var(--card) 70%, transparent); color: var(--ink); } -.site-header__gh:focus-visible { +.site-header__mark:focus-visible { outline: 2px solid var(--blueprint); outline-offset: 2px; } +/* The marks are a pair, so they sit tighter to each other than to the + surrounding actions. */ +.site-header__mark + .site-header__mark { + margin-inline-start: -6px; +} + .site-footer__gh { align-items: center; color: #d8d1c2; @@ -584,16 +611,23 @@ .feature-grid__cards { display: grid; - gap: 20px 44px; + gap: 20px; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 40px; } +/* Without a supporting sentence each, the cards need their own surface to + hold the band; the tile is the body. */ .feature-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--r-lg); + box-shadow: var(--shadow-sm); display: flex; flex-direction: column; - gap: 14px; + gap: 20px; min-width: 0; + padding: 26px 24px 28px; } .feature-card__glyph { @@ -621,24 +655,19 @@ } .feature-card__title { - font-size: 18px; - margin: 2px 0 0; -} - -.feature-card__description { - color: var(--ink-2); - font-size: 15px; - line-height: 1.6; - margin: -4px 0 0; - max-width: 31ch; + font-size: 19px; + margin: 0; + max-width: 18ch; } /* ===================================================================== */ /* CLI band — the terminal as a peer of the playground and the agent */ /* route, so the install command is the section rather than a link to it. */ /* ===================================================================== */ +/* The copy column is now a title, a line, and a link, so it centers against + the terminal card rather than hanging from its top edge. */ .cli-band__inner { - align-items: start; + align-items: center; display: grid; gap: 44px; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); @@ -656,8 +685,7 @@ max-width: 46ch; } -.cli-band__lead code, -.cli-band__point dd code { +.cli-band__lead code { background: var(--paper-2); border: 1px solid var(--line); border-radius: 5px; @@ -666,27 +694,6 @@ padding: 1px 5px; } -.cli-band__points { - display: grid; - gap: 16px; - margin: 26px 0 0; -} - -.cli-band__point dt { - color: var(--ink); - font-family: var(--font-display); - font-size: 16px; - font-weight: 700; -} - -.cli-band__point dd { - color: var(--ink-2); - font-size: 14.5px; - line-height: 1.6; - margin: 3px 0 0; - max-width: 44ch; -} - .cli-band__links { align-items: center; display: flex; @@ -702,17 +709,6 @@ flex: none; } -.cli-band__docs-link { - color: var(--accent-ink); - font-size: 15px; - font-weight: 700; - text-decoration: none; -} - -.cli-band__docs-link:hover { - text-decoration: underline; -} - /* Terminal card: same chrome as the hero board, dark body like a codeblock. */ .cli-band__terminal { background: var(--card); @@ -764,22 +760,14 @@ margin: 0; } -.cli-band__note { - color: var(--ink-3); - font-size: 13px; - line-height: 1.5; - margin: 8px 0 0; -} - -.cli-band__step + .cli-band__step { - margin-top: 18px; +.cli-band__body .code-snippet + .code-snippet { + margin-top: 12px; } -.cli-band__then { - color: var(--ink-2); - font-size: 14.5px; - font-weight: 600; - margin: 24px 0 10px; +/* The example command is a different kind of line from the two installers, so + it gets a wider gap rather than a label. */ +.cli-band__example { + margin-top: 24px; } .cli-band__req { @@ -1107,7 +1095,7 @@ } .docs-install .docs-codeblock { - margin-bottom: 6px; + margin-bottom: 12px; } .docs-install__note { @@ -1341,11 +1329,6 @@ padding: 10px; } -.agent-endpoint__meta { - color: var(--ink-3); - font-size: 13px; -} - .agent-list { padding-top: 34px; } @@ -1530,10 +1513,18 @@ .site-footer__inner { display: grid; gap: 36px; - grid-template-columns: 1.6fr repeat(3, 1fr); + grid-template-columns: 1.25fr repeat(3, 1fr); padding-block: 52px; } +/* The brand column is a wordmark and a source link; it stacks on its own + rather than relying on a paragraph between them to break the line. */ +.site-footer__about { + align-items: flex-start; + display: flex; + flex-direction: column; +} + .site-footer__brand { align-items: center; color: var(--paper-2); @@ -1544,14 +1535,6 @@ gap: 9px; } -.site-footer__tagline { - color: #a9a294; - font-size: 14px; - line-height: 1.6; - margin: 14px 0 0; - max-width: 32ch; -} - .site-footer__col h3 { color: #a9a294; font-family: var(--font-mono); @@ -1580,12 +1563,7 @@ .site-footer__npm { align-items: center; display: inline-flex; - gap: 7px; -} - -.site-footer__npm .brand-icon__img { - border-radius: 3px; - flex: none; + padding-block: 3px; } .site-footer__col a:hover { @@ -1652,7 +1630,7 @@ } .feature-grid__cards { - gap: 28px 36px; + gap: 16px; grid-template-columns: 1fr 1fr; } @@ -1662,8 +1640,7 @@ } .cli-band__title, - .cli-band__lead, - .cli-band__point dd { + .cli-band__lead { max-width: none; }