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
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 @@ -150,7 +149,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
101 changes: 101 additions & 0 deletions site/src/app/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,107 @@ 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;
}

// These caption paragraphs are scoped under .SkillsLanding__ecosystem so
// they outweigh the SDS global `.sds-theme-light p / .sds-theme-dark p`
// rule (specificity 0,1,1), which otherwise forces every <p> to 16px /
// 1.75rem line-height. A bare `.SkillsLanding__communityCount` (0,1,0)
// loses to it; the ancestor prefix (0,2,0) wins.
.SkillsLanding__ecosystem .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__ecosystem .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);
}

.SkillsLanding__ecosystem 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