is published on npm and authors
diagrams from your shell. Records live under{" "}
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.
@@ -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: (
{clusterIcons.map((icon) => (
@@ -41,15 +36,14 @@ const features: readonly Feature[] = [
title: "1,400+ logos, already drawn",
},
{
- description:
- "Open the scene in Excalidraw, then export it when you are ready.",
glyph: ,
title: "Yours to edit and export",
},
];
/**
- * Three icon-led benefits with one supporting sentence each.
+ * Three icon-led benefits. The titles say the whole thing; the cards carry the
+ * band's weight through their own surface rather than a sentence each.
*/
export function FeatureGrid() {
return (
@@ -63,7 +57,6 @@ export function FeatureGrid() {
{feature.glyph}
{feature.title}
- {feature.description}
))}
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({
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({
+
+
+
GitHub
+
+
+
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;
}