From ca6b91d883d68ae45f8491964428bf829bee7c91 Mon Sep 17 00:00:00 2001 From: anandpant <109482096+anandpant@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:21:26 +0000 Subject: [PATCH] fix(web): cut the copy, drop the Product tab, put npm in the header (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subtractive pass over `apps/web`, per the S5 brief. Three jobs plus a full read of the rest of the site. ## 1. Nav — Product removed `{ href: "/#product", label: "Product" }` is gone from the header. The `FeatureGrid` section keeps `id="product"`; nothing else in the repo linked to that anchor (grepped), so no dangling target. ## 2. npm beside GitHub `https://www.npmjs.com/package/sketchi` now sits in the header actions next to GitHub, in the same 40px `.site-header__mark` button with the same hover and focus treatment, and again in the mobile sheet where the desktop actions are hidden. npm now uses its **wordmark**, masked in `currentColor` exactly like `.gh-mark`, at its natural 46×18 rather than forced into a 20px square. It inherits ink on the light header and paper on the dark footer, and appears in the header bar, the mobile sheet, and the footer's More column. Two things about the asset, since neither was obvious: - **The square tile cannot be masked.** `/brand/npm.svg` is a knockout — a solid red 100×100 with the `n` cut out — so an alpha mask renders the complement: a bare frame with a bar in it. Shipping it in colour instead (the first push) read as a red blob, not as npm, and broke the header's monochrome treatment. - **npm's wordmark geometry cannot be masked verbatim either.** Its letters bleed to the left, top, and right edges of the red box, so the holes break the field's outline and the mask renders as disconnected slivers. `/brand/npm-wordmark.svg` therefore uses npm's own 780×250 letter grid **unchanged**, inset by a uniform 50 units. That inset is the only deviation and it is load-bearing. The footer's "npm package" entry takes the same mark, which drops two words — the wordmark is the word. ## 3. Every string deleted **CLI band** - "`sketchi` 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." - "Generate without signing in" - "One prompt, one HTTPS request, a validated diagram on disk. No API key, token, or account." - "Seven commands work offline" - "Create, patch, show, edit, list, export, and restore never touch the network or a model." - "Export from the terminal" - "Render PNG or Excalidraw locally, deterministically, without starting a browser." - "Installs the package and sets up completions for zsh, bash, or fish." - "Straight from npm, if you would rather configure completions yourself." - "Then ask for a diagram:" - "Read the CLI docs →" Replaced by one line: "`sketchi` draws diagrams from your shell. No account, no API key, and `--output json` on every command so an agent can drive it." **Feature grid** — the three sentences added in an earlier round: - "Every shape, connector, and label stays editable after generation." - "Find the tools in your stack without drawing their marks by hand." - "Open the scene in Excalidraw, then export it when you are ready." Nothing replaces them. The band was thin without them, so the cards got a surface, padding, and a tighter grid — layout, not words. **Closing CTA** - "or install the CLI →" **Footer** - "Prompts become real, editable diagrams, logos included." - "Made for people who'd rather describe a diagram than draw one." - "npm package" (the wordmark says it) Both restated the hero. The hero survives. **Docs** - "You describe it." (from the Three paths lead) - ", not a best-guess sketch" - ", so nothing you make is trapped in a browser tab" - "Makes a single credential-free HTTPS request — " → "No key, token, account, or login." - "PNG rendering is deterministic and local; it never starts a browser or uses the network." → "Local and deterministic; never uses the network." - "Read the code, file an issue, or run your own copy." - "Take it to your docs, slides, or pull request." → "Yes, as PNG or Excalidraw." - both install captions (shared with the CLI band above) **Agents (hub + all four pages)** - ", not a wall of ASCII" - "Add this URL wherever your agent keeps its MCP config." (endpoint caption, hub) - "Public endpoint used by the setup below." (endpoint caption, detail pages) - "Installs as a Codex plugin, so the skill is always a command away." - "Setup is just the skill plus the public MCP server." (×2) - "The plugin bundles the skill, so you can ask in plain language." - "; provider and model behavior remain OpenCode concerns" - "; model and harness behavior remain Agy concerns" Kept: "No account, API key, or local browser needed." on each page, and Antigravity's merge-don't-overwrite note, which is real setup information the heading does not carry. **Dead code removed with it:** `cliCapabilities`, `CliCapability`, `CliInstallOption.detail`, `CtaBandProps.cliHref`, `CliBandProps.docsHref`, `SiteFooterProps.colophon`, `EndpointCard`'s `meta` prop, and the CSS for `.cli-band__points`, `.cli-band__point`, `.cli-band__note`, `.cli-band__then`, `.cli-band__step`, `.cli-band__docs-link`, `.feature-card__description`, `.site-footer__tagline`, `.agent-endpoint__meta`. No caption, tooltip, helper line, or "learn more" was added anywhere. Install commands are byte-identical — `cli-package.test.ts` still pins them. ## Tests Rewritten to assert the absence rather than deleted: the CLI band now proves it makes *no* offline claim and captions nothing; the feature grid proves no `

` under a title; the CTA proves two actions; the footer proves the hero thesis is not restated; the header proves both marks reach the mobile sheet. ## Proof - `pnpm nx run-many -t typecheck,test,build` — green (52 tasks) - `pnpm test:tools` — 64 passed - Real Chrome at 1440×900 and 390×844: home, `/docs`, `/agents`, `/agents/codex`, `/agents/antigravity`. npm and GitHub both link correctly in the bar and the sheet, no Product tab, no empty container or dangling anchor from anything cut. - Both marks checked at 1x on the light header, the dark footer, and in the mobile sheet. - `codex review --base main` — clean on both pushes. --- apps/web/public/brand/npm-wordmark.svg | 12 ++ .../agent-setup-view/agent-setup-data.ts | 30 +--- .../agent-setup-view.test.tsx | 8 +- .../agent-setup-view/agent-setup-view.tsx | 9 +- .../src/components/cli-band/cli-band.test.tsx | 39 +++-- apps/web/src/components/cli-band/cli-band.tsx | 44 ++--- .../src/components/cta-band/cta-band.test.tsx | 16 +- apps/web/src/components/cta-band/cta-band.tsx | 10 +- .../src/components/docs-view/docs-view.tsx | 37 ++-- .../feature-grid/feature-grid.test.tsx | 25 ++- .../components/feature-grid/feature-grid.tsx | 11 +- .../site-footer/site-footer.test.tsx | 32 ++-- .../components/site-footer/site-footer.tsx | 16 +- .../site-header/site-header.test.tsx | 27 ++- .../components/site-header/site-header.tsx | 24 ++- apps/web/src/lib/cli-package.ts | 34 ---- apps/web/src/routes/-agents.test.tsx | 3 - apps/web/src/styles/app.css | 163 ++++++++---------- 18 files changed, 245 insertions(+), 295 deletions(-) create mode 100644 apps/web/public/brand/npm-wordmark.svg 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; }