Skip to content
Open
16 changes: 10 additions & 6 deletions site/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ client component.
`scripts/generate-llms-txt.mjs`.
- `src/app/page.tsx`: server-rendered landing page.
- `src/app/_components/`: `SkillCard` (server), `icons` (server, inline
SVGs), `CopyButton` / `SkillsFilter` / `ThemeSwitchIsland` (client
islands). `SkillsFilter` is used twice — once for the skills grid
(`All` + categories) and once for the Installing section's tabs
(Claude Code / Cursor / npx skills / Clone repo).
SVGs), `CopyButton` / `SkillsFilter` / `CommunitySearch` /
`ThemeSwitchIsland` (client islands). `SkillsFilter` is used twice — once
for the skills grid (`All` + categories) and once for the Installing
section's tabs (Claude Code / Cursor / npx skills / Clone repo).
`CommunitySearch` adds search + pagination to the community grid; the
cards render server-side and are passed in as children, so every entry
stays in the static HTML.
- `scripts/copy-skills.mjs`: mirrors `../skills/<source>` into
`public/<source>` on `predev` / `prebuild`. No network; just `cp`.
- `scripts/generate-llms-txt.mjs`: writes `public/llms.txt` from
Expand Down Expand Up @@ -80,15 +83,16 @@ then append to `SKILL_CARD_SOURCES`:
`pnpm sync:skills && pnpm dev` to verify. New category? Add it to the
`FilterType` union and the `FILTERS` array.

**Ecosystem:** external link, no upstream copy.
**Ecosystem:** external link, no upstream copy. New entries are picked
up automatically by the community search, pagination, and llms.txt; no
other wiring needed.

```ts
{
title: "Project Name",
description: "Verb-led summary of what the skill does.",
pathLabel: "owner/repo",
copyValue: "https://github.com/owner/repo/blob/main/path/to/SKILL.md",
category: "Ecosystem",
}
```

Expand Down
2 changes: 0 additions & 2 deletions site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ Append to `ECOSYSTEM_CARDS` in `src/data/skills.ts`:
description: "Verb-led summary of what the skill does.",
pathLabel: "owner/repo",
copyValue: "https://github.com/owner/repo/blob/main/path/to/SKILL.md",
category: "Ecosystem",
}
```

Expand All @@ -160,7 +159,6 @@ Append to `ECOSYSTEM_CARDS` in `src/data/skills.ts`:
- `copyValue` is the full URL written to the clipboard when the user
clicks the pill — point it directly at the raw SKILL.md so an agent
can fetch it.
- `category` is always `"Ecosystem"` for this section.

Run `pnpm dev` to verify the card renders. No `sync:skills` step
needed since ecosystem entries aren't mirrored locally.
Expand Down
3 changes: 1 addition & 2 deletions site/scripts/generate-llms-txt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const ecosystemCards = ECOSYSTEM_CARDS.map((c) => ({
title: c.title,
description: c.description,
copyValue: c.copyValue,
category: c.category,
}));
const filters = [...FILTERS];

Expand Down Expand Up @@ -137,7 +136,7 @@ if (realEcosystem.length > 0) {
lines.push("## Community Built");
lines.push("");
lines.push(
"Other community-built skills that may be helpful for your build. These aren't installed via the methods above; each project has its own setup, so follow the link on each entry. Not endorsed by the Stellar Foundation; do your own research.",
"Other community-built skills that may be helpful for your build. These aren't installed via the methods above; each project has its own setup, so follow the link on each entry. Not endorsed by the Stellar Foundation; do your own research. To get a skill listed here, open a pull request adding it to ECOSYSTEM_CARDS in site/src/data/skills.ts of https://github.com/stellar/stellar-dev-skill.",
);
lines.push("");
for (const c of realEcosystem) {
Expand Down
108 changes: 108 additions & 0 deletions site/src/app/_components/CommunitySearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"use client";

import { Children, ReactNode, useId, useState } from "react";

import { Input, Pagination } from "@stellar/design-system";

/** Cards shown per page in the 2-column community grid. */
const PAGE_SIZE = 8;

type Props = {
/** Lowercased "title description" haystack for each child, in the
* same render order as `children`. Search runs against these strings
* so the cards themselves stay server-rendered; this island only
* toggles wrapper visibility. */
searchTexts: readonly string[];
/** Pre-rendered card markup (server). One child per searchTexts
* entry, same order. */
children: ReactNode;
};

/**
* Client island that adds search and pagination to the community
* skills grid. The cards render server-side and are passed in as
* `children` (same slot pattern as `SkillsFilter`), so every card's
* title/description/link stays in the static HTML for non-JS clients;
* this component only hides wrappers that don't match the query or
* fall outside the current page.
*
* Search matches a case-insensitive substring of the card title or any
* part of its description, so a query like "DEX" finds a card that
* only mentions it mid-description.
*/
export const CommunitySearch = ({ searchTexts, children }: Props) => {
const searchInputId = useId();
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);

const needle = query.trim().toLowerCase();
const matches = searchTexts.reduce<number[]>((acc, text, index) => {
if (text.includes(needle)) acc.push(index);
return acc;
}, []);

// Clamp instead of trusting `page`: a new query can shrink the match
// list below the previously selected page.
const pageCount = Math.max(1, Math.ceil(matches.length / PAGE_SIZE));
const currentPage = Math.min(page, pageCount);
const visible = new Set(
matches.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE),
);

return (
<>
<div className="SkillsLanding__communitySearch">
<Input
id={searchInputId}
fieldSize="md"
type="search"
placeholder="Search community skills"
aria-label="Search community skills by title or description"
value={query}
onChange={(event) => {
setQuery(event.target.value);
setPage(1);
}}
/>
</div>

<div className="SkillsLanding__ecosystemGrid">
{Children.map(children, (child, index) => (
<div
key={index}
className="SkillsLanding__communityItem"
data-hidden={!visible.has(index)}
>
{child}
</div>
))}
</div>
Comment thread
Copilot marked this conversation as resolved.

{matches.length === 0 && (
<p className="SkillsLanding__communityEmpty">
No community skills match your search.
</p>
)}

<p
className="SkillsLanding__communityCount"
role="status"
aria-live="polite"
>
Showing {visible.size} of {searchTexts.length} skill
{searchTexts.length === 1 ? "" : "s"}
</p>

{matches.length > PAGE_SIZE && (
<div className="SkillsLanding__communityPagination">
<Pagination
pageSize={PAGE_SIZE}
itemCount={matches.length}
currentPage={currentPage}
setCurrentPage={setPage}
/>
</div>
)}
</>
);
};
43 changes: 42 additions & 1 deletion site/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/data/skills";
import { readSkillMeta } from "@/lib/skill-meta.mjs";

import { CommunitySearch } from "./_components/CommunitySearch";
import { CopyButton } from "./_components/CopyButton";
import { GitHubIcon, LinkExternal01Icon } from "./_components/icons";
import { SkillCard } from "./_components/SkillCard";
Expand Down Expand Up @@ -50,6 +51,15 @@ const hostFromOrigin = (origin: string) => origin.replace(/^https?:\/\//, "");
const githubSourceUrl = (source: string) =>
`https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SOURCE_REF}/${source}`;

// Example ECOSYSTEM_CARDS entry shown in the "Add your skill" block.
// Mirrors the format documented in site/CLAUDE.md.
const ADD_SKILL_SNIPPET = `{
title: "Project Name",
description: "Verb-led summary of what the skill does.",
pathLabel: "owner/repo",
copyValue: "https://github.com/owner/repo/blob/main/path/to/SKILL.md",
}`;

export default function LandingPage() {
const host = hostFromOrigin(SITE_ORIGIN);
const heroValue = `Read ${host} before you start building on Stellar.`;
Expand Down Expand Up @@ -188,7 +198,11 @@ export default function LandingPage() {
tool or resource. Inclusion in this list does not imply any
warranty, security audit, or official recommendation.
</p>
<div className="SkillsLanding__ecosystemGrid">
<CommunitySearch
searchTexts={ECOSYSTEM_CARDS.map((c) =>
`${c.title} ${c.description}`.toLowerCase(),
)}
>
{ECOSYSTEM_CARDS.map((c) => (
<SkillCard
key={c.copyValue}
Expand All @@ -200,6 +214,33 @@ export default function LandingPage() {
headingLevel={3}
/>
))}
</CommunitySearch>

<div className="SkillsLanding__addSkill">
<h3 className="SkillsLanding__addSkillTitle">Add your skill</h3>
<p className="SkillsLanding__addSkillText">
Built a skill that helps people develop on Stellar?{" "}
<a
href={`https://github.com/${GITHUB_REPOSITORY}/edit/main/site/src/data/skills.ts`}
target="_blank"
rel="noopener noreferrer"
>
Open a pull request
</a>{" "}
that adds an entry to <code>ECOSYSTEM_CARDS</code> in{" "}
<a
href={`https://github.com/${GITHUB_REPOSITORY}/blob/main/site/src/data/skills.ts`}
target="_blank"
rel="noopener noreferrer"
>
site/src/data/skills.ts
</a>
. Once merged, your skill shows up here and in{" "}
<a href={`${SITE_ORIGIN}/llms.txt`}>llms.txt</a>.
</p>
<pre className="SkillsLanding__addSkillSnippet">
<code>{ADD_SKILL_SNIPPET}</code>
</pre>
</div>
</section>
</main>
Expand Down
96 changes: 96 additions & 0 deletions site/src/app/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,102 @@ p.SkillsLanding__sectionDescription {
}
}

// Community search input (client island; the cards stay server-rendered).
// Width matches one grid column (50% minus half the 16px column gap) so
// the input lines up with the card below it.
.SkillsLanding__communitySearch {
width: calc(50% - #{pxToRem(8px)});
margin-bottom: pxToRem(16px);

@media (max-width: 600px) {
width: 100%;
}
}

// Each grid cell wraps one server-rendered card. `display: grid` makes
// the card stretch to the full row height, like a direct grid child.
.SkillsLanding__communityItem {
display: grid;
}

// Hidden by search/pagination. The card markup stays in the HTML so
// non-JS clients (AI agents, crawlers) still see every entry.
.SkillsLanding__communityItem[data-hidden="true"] {
display: none;
}

.SkillsLanding__communityEmpty {
font-family: "Inter", sans-serif;
font-size: pxToRem(12px);
line-height: pxToRem(18px);
color: var(--sds-clr-gray-11);
margin: pxToRem(8px) 0 0 0;
}

// "Showing x of y skills" status line under the community grid.
// Matches the ecosystem card title (h3.SkillsCard__title): 14px Inter
// semibold in gray-12.
.SkillsLanding__communityCount {
font-family: "Inter", sans-serif;
font-size: pxToRem(14px);
font-weight: 600;
line-height: pxToRem(20px);
color: var(--sds-clr-gray-12);
margin: pxToRem(12px) 0 0 0;
}

.SkillsLanding__communityPagination {
display: flex;
justify-content: center;
margin-top: pxToRem(16px);
}

// "Add your skill" block at the bottom of the community panel.
.SkillsLanding__addSkill {
margin-top: pxToRem(24px);
padding-top: pxToRem(16px);
border-top: 1px solid var(--sds-clr-gray-06);
}

h3.SkillsLanding__addSkillTitle {
font-family: "Inter", sans-serif;
font-size: pxToRem(14px);
font-weight: 600;
line-height: pxToRem(20px);
margin: 0 0 pxToRem(4px) 0;
color: var(--sds-clr-gray-12);
}

p.SkillsLanding__addSkillText {
font-family: "Inter", sans-serif;
font-size: pxToRem(12px);
font-weight: 400;
line-height: pxToRem(18px);
margin: 0 0 pxToRem(12px) 0;
color: var(--sds-clr-gray-11);

a {
color: var(--sds-clr-gray-12);
}

code {
font-family: "Roboto Mono", monospace;
}
}

.SkillsLanding__addSkillSnippet {
font-family: "Roboto Mono", monospace;
font-size: pxToRem(12px);
line-height: pxToRem(18px);
color: var(--sds-clr-gray-11);
background-color: var(--sds-clr-gray-03);
border: 1px solid var(--sds-clr-gray-06);
border-radius: pxToRem(8px);
padding: pxToRem(12px);
margin: 0;
overflow-x: auto;
}

// Installer panel: independent tablist; only the active tab's panel
// shows. Stacks via flex column because exactly one panel is visible.
.SkillsLanding__installerPanel {
Expand Down
5 changes: 0 additions & 5 deletions site/src/data/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export type EcosystemCardSource = {
pathLabel: string;
/** Full URL copied to clipboard when the user clicks the pill. */
copyValue: string;
category: FilterType;
};

/**
Expand Down Expand Up @@ -142,7 +141,6 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "OpenZeppelin/openzeppelin-skills",
copyValue:
"https://github.com/OpenZeppelin/openzeppelin-skills/blob/main/skills/setup-stellar-contracts/SKILL.md",
category: "Ecosystem",
},
{
title: "DeFindex SDK",
Expand All @@ -151,15 +149,13 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "paltalabs/defindex-sdk",
copyValue:
"https://github.com/paltalabs/defindex-sdk/blob/main/defindex-sdk-skill.md",
category: "Ecosystem",
},
{
title: "Soroswap SDK",
description:
"Trade on Soroswap DEX from a backend, bot, or swap widget using the @soroswap/sdk TypeScript package. Covers token swaps, liquidity pool operations, price and route queries, API key handling, and signing flows for both server keypairs and browser wallets.",
pathLabel: "soroswap/sdk",
copyValue: "https://github.com/soroswap/sdk/blob/main/soroswap-sdk-skill.md",
category: "Ecosystem",
},
{
title: "Trustless Work Escrow",
Expand All @@ -168,6 +164,5 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [
pathLabel: "Trustless-Work/trustless-work-dev-skill",
copyValue:
"https://github.com/Trustless-Work/trustless-work-dev-skill/blob/main/SKILL.md",
category: "Ecosystem",
},
] as const;
Loading