diff --git a/site/CLAUDE.md b/site/CLAUDE.md index e6e05b1..00c0061 100644 --- a/site/CLAUDE.md +++ b/site/CLAUDE.md @@ -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/` into `public/` on `predev` / `prebuild`. No network; just `cp`. - `scripts/generate-llms-txt.mjs`: writes `public/llms.txt` from @@ -80,7 +83,9 @@ 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 { @@ -88,7 +93,6 @@ then append to `SKILL_CARD_SOURCES`: 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", } ``` diff --git a/site/README.md b/site/README.md index 13506f5..4a97fdc 100644 --- a/site/README.md +++ b/site/README.md @@ -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", } ``` @@ -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. diff --git a/site/scripts/generate-llms-txt.mjs b/site/scripts/generate-llms-txt.mjs index bd7afd0..a822c54 100644 --- a/site/scripts/generate-llms-txt.mjs +++ b/site/scripts/generate-llms-txt.mjs @@ -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]; @@ -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) { diff --git a/site/src/app/_components/CommunitySearch.tsx b/site/src/app/_components/CommunitySearch.tsx new file mode 100644 index 0000000..d6fdaaa --- /dev/null +++ b/site/src/app/_components/CommunitySearch.tsx @@ -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((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 ( + <> +
+ { + setQuery(event.target.value); + setPage(1); + }} + /> +
+ +
+ {Children.map(children, (child, index) => ( +
+ {child} +
+ ))} +
+ + {matches.length === 0 && ( +

+ No community skills match your search. +

+ )} + +

+ Showing {visible.size} of {searchTexts.length} skill + {searchTexts.length === 1 ? "" : "s"} +

+ + {matches.length > PAGE_SIZE && ( +
+ +
+ )} + + ); +}; diff --git a/site/src/app/page.tsx b/site/src/app/page.tsx index ab3c0cf..0828915 100644 --- a/site/src/app/page.tsx +++ b/site/src/app/page.tsx @@ -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"; @@ -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.`; @@ -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.

-
+ + `${c.title} ${c.description}`.toLowerCase(), + )} + > {ECOSYSTEM_CARDS.map((c) => ( ))} + + +
+

Add your skill

+

+ Built a skill that helps people develop on Stellar?{" "} + + Open a pull request + {" "} + that adds an entry to ECOSYSTEM_CARDS in{" "} + + site/src/data/skills.ts + + . Once merged, your skill shows up here and in{" "} + llms.txt. +

+
+              {ADD_SKILL_SNIPPET}
+            
diff --git a/site/src/app/styles.scss b/site/src/app/styles.scss index bf2b319..50a90b9 100644 --- a/site/src/app/styles.scss +++ b/site/src/app/styles.scss @@ -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 { diff --git a/site/src/data/skills.ts b/site/src/data/skills.ts index fcd5960..3db3892 100644 --- a/site/src/data/skills.ts +++ b/site/src/data/skills.ts @@ -71,7 +71,6 @@ export type EcosystemCardSource = { pathLabel: string; /** Full URL copied to clipboard when the user clicks the pill. */ copyValue: string; - category: FilterType; }; /** @@ -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", @@ -151,7 +149,6 @@ 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", @@ -159,7 +156,6 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [ "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", @@ -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;