Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"shiki": "^4.0.2",
"simple-icons": "^16.30.0",
"tailwindcss": "^4.2.2",
"unified": "^11.0.5"
},
Expand Down
Binary file added public/docs/gentle-ai/rose.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions src/components/docs/AgentGrid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
/**
* The "pick your agent" grid of /docs/agent-setup.
*
* It replaced a table. A table is the right shape for comparing numbers, and
* the wrong one for choosing: a reader arrives knowing which tool they use and
* wants to find it, which is a recognition task. Marks and cards are scanned
* in one pass; a column of names is read line by line.
*
* `note` carries the one thing that changes the decision, not a description.
* For most tools that is nothing at all, and an empty note is better than a
* filler sentence repeated nine times.
*/
import AgentLogo from './AgentLogo.astro';

interface Item {
name: string;
href: string;
/** Terminal, IDE, extension… shown as the card's eyebrow. */
kind: string;
/** simple-icons key; omitted when the tool has no mark in the set. */
logo?: string;
/** The caveat worth knowing before clicking. */
note?: string;
/** Marks the tool that does not connect straight to the cluster. */
indirect?: boolean;
/** Marks the recommendation, so the eye lands on it first. */
featured?: boolean;
}

interface Props {
items: Item[];
}

const { items } = Astro.props;
---

<div class="not-prose my-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{
items.map((item) => (
<a
href={item.href}
class:list={[
'agent-card group flex flex-col gap-3 rounded-xl border p-5 transition-colors',
item.featured
? 'border-violet-500/40 bg-violet-500/[0.06] hover:border-violet-400/60'
: 'border-neutral-800/60 bg-[#0a0a0a] hover:border-neutral-700',
]}
>
<span class="flex items-center gap-3 text-neutral-300 transition-colors group-hover:text-violet-300">
<AgentLogo logo={item.logo} name={item.name} />
<span class="font-mono text-sm text-white">{item.name}</span>
</span>

<span class="flex flex-wrap items-center gap-2">
<span class="font-mono text-[10px] uppercase tracking-widest text-neutral-500">
{item.kind}
</span>
{item.indirect && (
<span class="rounded-[3px] border border-amber-500/30 px-1.5 py-px font-mono text-[10px] text-amber-300/90">
via pasarela
</span>
)}
{item.featured && (
<span class="rounded-[3px] border border-violet-500/40 px-1.5 py-px font-mono text-[10px] text-violet-300">
recomendado
</span>
)}
</span>

{item.note && <span class="text-xs leading-relaxed text-neutral-400">{item.note}</span>}
</a>
))
}
</div>

<style>
/*
* `.docs-content a` underlines every link in the prose, which is right for a
* link inside a sentence and wrong for a card: it underlined the title, the
* eyebrow and the whole note, and the grid read as a wall of links rather
* than as things to pick from. The card is the target; the words inside it
* are not.
*/
.agent-card,
.agent-card :global(*) {
text-decoration: none;
}
</style>
120 changes: 120 additions & 0 deletions src/components/docs/AgentLogo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
/**
* The mark of a coding agent, for the picker on /docs/agent-setup.
*
* Paths come from `simple-icons` rather than from SVG files committed here:
* the package is maintained, the marks stay current, and nothing in this repo
* has to be redrawn when a tool rebrands. Everything is resolved at build time,
* so the reader downloads a handful of inline paths and no icon library.
*
* Not every tool has one. Continue, Aider, Codex, VS Code and our own CLI are
* missing from the set, so they fall back to a monogram tile instead of to an
* empty square: a deliberate letterform reads as a choice, a blank does not.
*
* The marks render in the current text colour rather than in each brand's
* colour. That is the monoaccent rule of the brand, and it is also what keeps
* a grid of nine logos from looking like a sponsor wall.
*/
/*
* Named imports, one per mark, and never `import * as icons`: the package
* carries a few thousand of them, and a namespace import defeats tree-shaking,
* so all of them would be bundled into a Worker that has a size limit.
*
* Adding a tool here is one import plus one line in MARKS.
*/
import {
siClaudecode,
siCline,
siCursor,
siGithubcopilot,
siOpencode,
siPi,
siZedindustries,
} from 'simple-icons';

/*
* One mark that shares a name with something else is NOT here on purpose:
* `siHermes` in that package is myhermes.de, a German parcel courier, and not
* the agent by Nous Research. A wrong logo is worse than no logo, because it
* makes the page look like it knows something it does not.
*
* `siPi` is the right one, checked against its source: pi.dev, the terminal
* agent that configures itself under `~/.pi/agent/`.
*/

/*
* Marks that simple-icons does not carry.
*
* Same shape as the ones it does: a single path on a 24x24 viewBox, drawn in
* `currentColor`. Taken from lobehub/lobe-icons (MIT), which is where the
* OpenAI family lives now that simple-icons no longer publishes it.
*/
const LOCAL_MARKS: Record<string, { path: string }> = {
codex: {
path: 'M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z',
},
};

const MARKS: Record<string, { path: string }> = {
...LOCAL_MARKS,
claudecode: siClaudecode,
cline: siCline,
cursor: siCursor,
githubcopilot: siGithubcopilot,
opencode: siOpencode,
pi: siPi,
zedindustries: siZedindustries,
};

interface Props {
/** Key into MARKS. Omitted, or unknown, falls back to a monogram. */
logo?: string;
/** Used for the monogram fallback and for the accessible name. */
name: string;
size?: number;
}

const { logo, name, size = 22 } = Astro.props;

const icon = logo ? MARKS[logo.toLowerCase()] : undefined;

/**
* One or two letters, skipping the words that every tool shares. "GitHub
* Copilot" reads as GC, but "Visual Studio Code" as VS rather than VSC, which
* does not fit the tile at this size.
*/
const monogram = name
.replace(/\b(code|cli|ai|studio)\b/gi, '')
.split(/[\s.-]+/)
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0])
.join('')
.toUpperCase()
.slice(0, 2);
---

{
icon ? (
<svg
role="img"
aria-label={name}
viewBox="0 0 24 24"
width={size}
height={size}
fill="currentColor"
class="shrink-0"
>
<path d={icon.path} />
</svg>
) : (
<span
aria-label={name}
role="img"
class="grid shrink-0 place-items-center rounded-[4px] border border-current/25 font-mono font-medium leading-none"
style={`width:${size}px;height:${size}px;font-size:${Math.round(size * 0.42)}px`}
>
{monogram}
</span>
)
}
58 changes: 58 additions & 0 deletions src/components/docs/BrandIntro.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
/**
* A third-party project's own mark, beside the paragraph that introduces it.
*
* It exists so that raw `<div>`, `<img>` and `<style>` do not have to be
* written into the MDX. The text extractor only knows headings and a handful
* of inline tags, so any of those would throw and take /api/docs down for the
* page rather than degrade.
*
* The mark keeps its own colours. The monoaccent rule governs NaN's own
* surfaces; reproducing someone else's logo in violet would be recolouring
* their brand, which is worse than the exception. The exception ends here
* though: the picker on /docs/agent-setup uses monochrome marks throughout.
*/

interface Props {
src: string;
alt: string;
/** Intrinsic size, so the page does not reflow while the image loads. */
width: number;
height: number;
}

const { src, alt, width, height } = Astro.props;
---

<div class="brand-intro not-prose">
<img src={src} alt={alt} width={width} height={height} loading="lazy" decoding="async" />
<div class="brand-intro-body"><slot /></div>
</div>

<style>
.brand-intro {
display: flex;
align-items: center;
gap: 1.5rem;
margin: 1.5rem 0 2rem;
}

.brand-intro img {
width: 96px;
height: auto;
flex-shrink: 0;
border-radius: 0.75rem;
}

.brand-intro-body :global(p) {
margin: 0;
}

@media (max-width: 620px) {
.brand-intro {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}
</style>
Loading
Loading