diff --git a/.caplets.lock.json b/.caplets.lock.json
new file mode 100644
index 00000000..d4b08032
--- /dev/null
+++ b/.caplets.lock.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "entries": [
+ {
+ "id": "github",
+ "destination": "github",
+ "kind": "directory",
+ "source": {
+ "type": "local",
+ "path": "/tmp/caplets-cli-remote-install-explicit-project-0UFEXG/project/.caplets/repo/caplets/github",
+ "portability": "non_portable"
+ },
+ "installedHash": "sha256:73c123669a3f22f683156dc2746d8b21442078be5e7463686506ae9057381cd6",
+ "installedAt": "2026-06-26T19:27:24.767Z",
+ "updatedAt": "2026-06-26T19:27:24.809Z",
+ "risk": {
+ "backendFamilies": ["mcp"],
+ "safety": "local_control",
+ "projectBindingRequired": false,
+ "mutating": false,
+ "destructive": false,
+ "bodyHash": "sha256:73c123669a3f22f683156dc2746d8b21442078be5e7463686506ae9057381cd6"
+ }
+ }
+ ]
+}
diff --git a/.changeset/bright-icons-wave.md b/.changeset/bright-icons-wave.md
new file mode 100644
index 00000000..6cc46924
--- /dev/null
+++ b/.changeset/bright-icons-wave.md
@@ -0,0 +1,5 @@
+---
+"@caplets/core": minor
+---
+
+Add optional `catalog.icon` metadata to Caplet files and catalog entries so catalog surfaces can show provider icons.
diff --git a/.changeset/config.json b/.changeset/config.json
index d94dbced..2cca2dcb 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": ["@caplets/landing", "@caplets/docs"]
+ "ignore": ["@caplets/landing", "@caplets/docs", "@caplets/catalog"]
}
diff --git a/.changeset/fuzzy-catalogs-smile.md b/.changeset/fuzzy-catalogs-smile.md
new file mode 100644
index 00000000..f35895c9
--- /dev/null
+++ b/.changeset/fuzzy-catalogs-smile.md
@@ -0,0 +1,7 @@
+---
+"@caplets/core": minor
+"caplets": minor
+---
+
+Add shared catalog primitives, official catalog generation, public catalog indexing statuses,
+and install-time Vault setup recovery metadata for Caplets install/update flows.
diff --git a/.changeset/prebuilt-caplets-lockfile-update.md b/.changeset/prebuilt-caplets-lockfile-update.md
deleted file mode 100644
index 818e6b46..00000000
--- a/.changeset/prebuilt-caplets-lockfile-update.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"@caplets/core": minor
-"caplets": minor
----
-
-Add lockfile-aware Caplet install, restore, and update workflows, including `caplets update`, JSON lifecycle output, remote-global catalog mutations, derived update risk checks, and new public catalog entries for browser, desktop, observability, and Google Workspace integrations.
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 587bd9a6..c9f177c7 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -47,7 +47,7 @@ jobs:
- name: Run quality gates
run: pnpm verify
- - name: Deploy landing page
+ - name: Deploy sites
run: pnpm run alchemy:deploy
env:
ALCHEMY_STAGE: prod
diff --git a/.gitignore b/.gitignore
index c09ce3a0..9da68f70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,9 @@ benchmark-results/
# brag
brag-output/
+
+# wrangler
+.wrangler/
+
+# Test-generated Caplets Lockfiles
+.caplets.lock.json
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index 17a8580d..af2a8ae5 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
- "ignorePatterns": ["**/.brv/**"],
+ "ignorePatterns": ["**/.brv/**", "apps/catalog/src/data/official-catalog.json"],
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
diff --git a/CONCEPTS.md b/CONCEPTS.md
index 1bf3f2e6..74d1daab 100644
--- a/CONCEPTS.md
+++ b/CONCEPTS.md
@@ -16,12 +16,30 @@ The Prebuilt Caplets Catalog is curated as a Code Mode-first capability catalog,
Install-ready catalog entries have an explicit verification status, a reproducible validation path, and a named primary Code Mode workflow. Unverified entries may exist as drafts or recipes, but they do not count as install-ready catalog coverage.
+### Catalog Search Site
+
+The public search surface for installable Caplets at `catalog.caplets.dev`.
+
+The Catalog Search Site is separate from the landing page and docs site. It indexes official Caplets from this repo and community Caplets discovered through public external installs, with search, readable Caplet content, source labels, install-count popularity signals, warnings, and inspection-first copyable install commands as the primary user flow.
+
+### Public Catalog Indexing Signal
+
+The public-source install signal that lets `catalog.caplets.dev` discover and rank community Caplets.
+
+Public Catalog Indexing is not ordinary anonymous telemetry. It may publish public Caplet content, normalized install command, source identity needed to reproduce install, and aggregate install count; it must not publish installer identity, private source URLs, local paths, config, credentials, prompts, tool arguments, tool outputs, or hostnames. Catalog install counts are popularity and ranking signals, not safety signals, and public entries can be suppressed when automatic indexing creates stale, abusive, leaked, or high-risk catalog records.
+
### Catalog-Grade Caplet
A Caplet that is ready to live in the Prebuilt Caplets Catalog.
Catalog-Grade Caplets include enough frontmatter, setup or verification guidance, auth handling, least-privilege scope notes, safety notes, Code Mode workflow guidance, and local/project/runtime metadata for agents to use them without rediscovering private assumptions from the author's environment.
+### Catalog Presentation Metadata
+
+Optional Caplet frontmatter that improves how a Caplet appears in public catalog surfaces without changing runtime behavior, trust, safety status, ranking, or install readiness.
+
+In v1, Catalog Presentation Metadata is limited to `catalog.icon`, which may identify a safe HTTPS icon URL or a bundled icon path relative to the Caplet directory.
+
### Caplets Lockfile
A `caplets.lock.json` file that records installed catalog Caplets, their source repository, source path, destination, tracked source channel, resolved revision when available, content hash, and portability status.
diff --git a/README.md b/README.md
index 2848833d..20bbf861 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@
caplets.dev
·
docs.caplets.dev
+ ·
+ catalog.caplets.dev
diff --git a/alchemy.run.ts b/alchemy.run.ts
index e1d5a405..cebb192a 100644
--- a/alchemy.run.ts
+++ b/alchemy.run.ts
@@ -1,5 +1,5 @@
import alchemy from "alchemy";
-import { Astro } from "alchemy/cloudflare";
+import { Astro, D1Database } from "alchemy/cloudflare";
import { GitHubComment } from "alchemy/github";
import { CloudflareStateStore } from "alchemy/state";
@@ -10,28 +10,52 @@ const app = await alchemy("caplets", {
password: process.env.ALCHEMY_PASSWORD!,
});
-const { docsPageDomain, docsPageUrl, landingPageDomain, landingPageUrl } = buildAlchemyDomains(
- app.stage,
- { local: app.local },
-);
+const {
+ catalogPageDomain,
+ catalogPageUrl,
+ docsPageDomain,
+ docsPageUrl,
+ landingPageDomain,
+ landingPageUrl,
+} = buildAlchemyDomains(app.stage, { local: app.local });
+const hostSuffix = process.env.SSH_CONNECTION ? " --host 0.0.0.0" : "";
export const landingPage = await Astro("landing-page", {
cwd: "apps/landing",
dev: {
- command: "pnpm run dev" + (process.env.SSH_CONNECTION ? " --host 0.0.0.0" : ""),
+ command: "pnpm run dev --port 4321" + hostSuffix,
},
domains: [landingPageDomain, `www.${landingPageDomain}`],
});
export const docsPage = await Astro("docs-page", {
cwd: "apps/docs",
dev: {
- command: "pnpm run dev -- --port 4322" + (process.env.SSH_CONNECTION ? " --host 0.0.0.0" : ""),
+ command: "pnpm run dev --port 4322" + hostSuffix,
},
domains: [docsPageDomain],
});
+export const catalogDatabase = await D1Database("catalog-database", {
+ name: `caplets-${app.stage}-catalog`,
+ migrationsDir: "apps/catalog/migrations",
+ adopt: true,
+ delete: false,
+});
+export const catalogPage = await Astro("catalog-page", {
+ cwd: "apps/catalog",
+ entrypoint: "dist/server/entry.mjs",
+ assets: "dist/client",
+ dev: {
+ command: "pnpm run dev --port 4323" + hostSuffix,
+ },
+ bindings: {
+ CATALOG_DB: catalogDatabase,
+ },
+ domains: [catalogPageDomain],
+});
console.log({
"Landing Page URL": landingPageUrl,
"Docs Page URL": docsPageUrl,
+ "Catalog Page URL": catalogPageUrl,
});
const [repositoryOwnerFromSlug, repositoryNameFromSlug] =
@@ -55,6 +79,7 @@ if (pullRequestNumber) {
Landing: ${landingPageUrl}
Docs: ${docsPageUrl}
+Catalog: ${catalogPageUrl}
Built from commit ${shortSha}`,
});
diff --git a/apps/catalog/astro.config.mjs b/apps/catalog/astro.config.mjs
new file mode 100644
index 00000000..b8a170b5
--- /dev/null
+++ b/apps/catalog/astro.config.mjs
@@ -0,0 +1,50 @@
+// @ts-check
+import cloudflare from "@astrojs/cloudflare";
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from "astro/config";
+import { fileURLToPath } from "node:url";
+
+const cloudflareDevMiddleware = fileURLToPath(
+ new URL("./src/cloudflare-dev-middleware.ts", import.meta.url),
+);
+const cloudflareDevWorkers = fileURLToPath(
+ new URL("./src/cloudflare-workers-dev.ts", import.meta.url),
+);
+const isProductionBuild = process.env.NODE_ENV === "production";
+const optimizeExclude = [
+ "tailwind-variants",
+ "unified",
+ "remark-parse",
+ "remark-rehype",
+ "rehype-sanitize",
+ "rehype-stringify",
+];
+
+export default defineConfig({
+ site: "https://catalog.caplets.dev",
+ output: "server",
+ adapter: cloudflare(),
+ devToolbar: {
+ enabled: false,
+ },
+ vite: {
+ build: {
+ assetsInlineLimit: 0,
+ },
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ "@astrojs/cloudflare/entrypoints/middleware.js": cloudflareDevMiddleware,
+ ...(isProductionBuild ? {} : { "cloudflare:workers": cloudflareDevWorkers }),
+ },
+ },
+ ssr: {
+ optimizeDeps: {
+ exclude: optimizeExclude,
+ },
+ },
+ optimizeDeps: {
+ exclude: optimizeExclude,
+ },
+ },
+});
diff --git a/apps/catalog/migrations/0001_catalog.sql b/apps/catalog/migrations/0001_catalog.sql
new file mode 100644
index 00000000..9c93b2cf
--- /dev/null
+++ b/apps/catalog/migrations/0001_catalog.sql
@@ -0,0 +1,38 @@
+create table if not exists catalog_counts (
+ entry_key text primary key,
+ install_count integer not null default 0,
+ updated_at_ms integer not null
+);
+
+create table if not exists catalog_entries (
+ entry_key text primary key,
+ provider text not null,
+ repository text not null,
+ source_path text not null,
+ caplet_id text not null,
+ resolved_revision text,
+ content_hash text,
+ entry_json text not null,
+ updated_at_ms integer not null
+);
+
+create table if not exists catalog_signal_dedupe (
+ entry_key text not null,
+ signal_fingerprint text not null,
+ provider text not null,
+ repository text not null,
+ window_start_ms integer not null,
+ accepted_at_ms integer not null,
+ primary key (entry_key, signal_fingerprint, window_start_ms)
+);
+
+create table if not exists catalog_suppressions (
+ entry_key text primary key,
+ reason text not null,
+ suppressed_at text not null
+);
+
+create index if not exists catalog_counts_rank_idx on catalog_counts (install_count desc);
+create index if not exists catalog_entries_repository_idx on catalog_entries (provider, repository);
+create index if not exists catalog_signal_dedupe_entry_time_idx on catalog_signal_dedupe (entry_key, accepted_at_ms);
+create index if not exists catalog_signal_dedupe_repository_window_idx on catalog_signal_dedupe (provider, repository, window_start_ms);
diff --git a/apps/catalog/package.json b/apps/catalog/package.json
new file mode 100644
index 00000000..2dc85314
--- /dev/null
+++ b/apps/catalog/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@caplets/catalog",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "astro": "astro",
+ "build": "astro build",
+ "dev": "astro dev",
+ "preview": "astro preview",
+ "test": "vitest run test",
+ "typecheck": "astro check"
+ },
+ "dependencies": {
+ "@astrojs/check": "^0.9.9",
+ "@astrojs/cloudflare": "^13.7.0",
+ "@caplets/core": "workspace:*",
+ "@hugeicons/core-free-icons": "^4.2.2",
+ "@tabler/icons": "^3.44.0",
+ "@tailwindcss/forms": "^0.5.11",
+ "@tailwindcss/vite": "^4.3.1",
+ "@tanstack/virtual-core": "^3.17.2",
+ "astro": "^6.4.6",
+ "rehype-sanitize": "^6.0.0",
+ "rehype-stringify": "^10.0.1",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
+ "tailwind-variants": "^3.2.2",
+ "tailwindcss": "^4.3.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^6.0.3",
+ "unified": "^11.0.5"
+ },
+ "devDependencies": {
+ "happy-dom": "^20.10.6",
+ "vite": "^7.3.5",
+ "vitest": "^4.1.8",
+ "wrangler": "^4.105.0"
+ },
+ "packageManager": "pnpm@11.7.0"
+}
diff --git a/apps/catalog/public/icon-header-dark.png b/apps/catalog/public/icon-header-dark.png
new file mode 100644
index 00000000..7f54aa66
Binary files /dev/null and b/apps/catalog/public/icon-header-dark.png differ
diff --git a/apps/catalog/public/icon.png b/apps/catalog/public/icon.png
new file mode 100644
index 00000000..4eebda4b
Binary files /dev/null and b/apps/catalog/public/icon.png differ
diff --git a/apps/catalog/src/cloudflare-dev-middleware.ts b/apps/catalog/src/cloudflare-dev-middleware.ts
new file mode 100644
index 00000000..07e111b0
--- /dev/null
+++ b/apps/catalog/src/cloudflare-dev-middleware.ts
@@ -0,0 +1,3 @@
+import type { MiddlewareHandler } from "astro";
+
+export const onRequest: MiddlewareHandler = (_context, next) => next();
diff --git a/apps/catalog/src/cloudflare-workers-dev.ts b/apps/catalog/src/cloudflare-workers-dev.ts
new file mode 100644
index 00000000..1c60631f
--- /dev/null
+++ b/apps/catalog/src/cloudflare-workers-dev.ts
@@ -0,0 +1 @@
+export const env = {};
diff --git a/apps/catalog/src/components/CapletDetail.astro b/apps/catalog/src/components/CapletDetail.astro
new file mode 100644
index 00000000..bc2435d1
--- /dev/null
+++ b/apps/catalog/src/components/CapletDetail.astro
@@ -0,0 +1,134 @@
+---
+import { ChevronRightIcon } from "@hugeicons/core-free-icons";
+import type { CatalogEntryRecord } from "../lib/catalog-store";
+import { renderCatalogMarkdown, splitCatalogMarkdown } from "../lib/markdown";
+import HugeIcon from "./HugeIcon.astro";
+import InstallCommand from "./InstallCommand.astro";
+import SafetyNotice from "./SafetyNotice.astro";
+import { Badge } from "./starwind/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "./starwind/card";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./starwind/table";
+
+type Props = {
+ entry: CatalogEntryRecord;
+};
+
+const { entry } = Astro.props;
+const markdownParts = entry.contentMarkdown ? splitCatalogMarkdown(entry.contentMarkdown) : undefined;
+const renderedContent = markdownParts?.bodyMarkdown
+ ? await renderCatalogMarkdown(markdownParts.bodyMarkdown)
+ : undefined;
+const frontmatterRows = markdownParts?.frontmatterRows ?? [];
+const hasReadableContent = Boolean(renderedContent) || frontmatterRows.length > 0;
+const detailCommand = hasReadableContent
+ ? entry.installCommand
+ : { ...entry.installCommand, copyable: false };
+---
+
+
+
+
+
+ {entry.icon ? (
+

+ ) : (
+
+ )}
+
+
+ {entry.trustLevel}
+ {entry.source.repository}
+
+
{entry.name}
+
{entry.description}
+
+
+
+ Install
+
+
+
+
+
+
+
+
+
+ CAPLET.md
+
+
+ {
+ hasReadableContent ? (
+ <>
+ {frontmatterRows.length > 0 && (
+
+ Frontmatter
+
+
+
+
+ Field
+ Value
+
+
+
+ {frontmatterRows.map((row) => (
+
+
+ {row.key}
+ |
+ {row.value}
+
+ ))}
+
+
+
+
+ )}
+ {renderedContent && }
+ >
+ ) : (
+
+
Content unavailable
+
This entry cannot be installed from the catalog until readable Caplet content is available for inspection.
+
+ )
+ }
+
+
+
+
+
+
diff --git a/apps/catalog/src/components/CapletResult.astro b/apps/catalog/src/components/CapletResult.astro
new file mode 100644
index 00000000..99f2eec2
--- /dev/null
+++ b/apps/catalog/src/components/CapletResult.astro
@@ -0,0 +1,50 @@
+---
+import type { CatalogEntryRecord } from "../lib/catalog-store";
+import InstallCommand from "./InstallCommand.astro";
+import SafetyNotice from "./SafetyNotice.astro";
+import { Badge } from "./starwind/badge";
+import { Button } from "./starwind/button";
+
+type Props = {
+ entry: CatalogEntryRecord;
+};
+
+const { entry } = Astro.props;
+const detailHref = `/caplets/${encodeURIComponent(entry.entryKey)}/`;
+---
+
+
+
+
+
{entry.description}
+
+ {entry.source.repository}
+ {entry.workflow.label}
+ {entry.installCountDisplay} installs
+ {entry.authReadiness === "required" ? "auth required" : "auth ready"}
+
+ {entry.tags.length > 0 && (
+
+ {entry.tags.map((tag) => {tag})}
+
+ )}
+
+
+
+
+
+
+
diff --git a/apps/catalog/src/components/CatalogHeader.astro b/apps/catalog/src/components/CatalogHeader.astro
new file mode 100644
index 00000000..47522eac
--- /dev/null
+++ b/apps/catalog/src/components/CatalogHeader.astro
@@ -0,0 +1,235 @@
+---
+import { Cancel01Icon } from "@hugeicons/core-free-icons";
+import HugeIcon from "./HugeIcon.astro";
+import ThemeToggle from "./ThemeToggle.astro";
+
+type SearchProps = {
+ count?: number;
+ value?: string;
+};
+
+type Props = {
+ search?: SearchProps;
+};
+
+const { search } = Astro.props;
+---
+
+
+
+
+
+
diff --git a/apps/catalog/src/components/FilterBar.astro b/apps/catalog/src/components/FilterBar.astro
new file mode 100644
index 00000000..98100d16
--- /dev/null
+++ b/apps/catalog/src/components/FilterBar.astro
@@ -0,0 +1,46 @@
+---
+import { Select, SelectContent, SelectItem, SelectSearch, SelectTrigger, SelectValue } from "./starwind/select";
+
+const tags = Astro.props.tags as string[];
+---
+
+
+
+
+
+ Tag
+
+
+
+
+
diff --git a/apps/catalog/src/components/HugeIcon.astro b/apps/catalog/src/components/HugeIcon.astro
new file mode 100644
index 00000000..ed76d281
--- /dev/null
+++ b/apps/catalog/src/components/HugeIcon.astro
@@ -0,0 +1,39 @@
+---
+type IconNode = readonly [string, Readonly>];
+
+type Props = {
+ icon: readonly IconNode[];
+ label?: string;
+ class?: string;
+};
+
+const { icon, label, class: className } = Astro.props;
+
+function svgAttrs(attrs: IconNode[1]): Record {
+ const normalized: Record = {};
+ for (const [key, value] of Object.entries(attrs)) {
+ if (key === "key") continue;
+ normalized[key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)] = value;
+ }
+ return normalized;
+}
+---
+
+
diff --git a/apps/catalog/src/components/InstallCommand.astro b/apps/catalog/src/components/InstallCommand.astro
new file mode 100644
index 00000000..7562ca40
--- /dev/null
+++ b/apps/catalog/src/components/InstallCommand.astro
@@ -0,0 +1,34 @@
+---
+import type { CatalogInstallCommand } from "@caplets/core/catalog";
+import { Copy01Icon } from "@hugeicons/core-free-icons";
+import HugeIcon from "./HugeIcon.astro";
+import { Button } from "./starwind/button";
+
+type Props = {
+ command: CatalogInstallCommand;
+ detail?: boolean;
+};
+
+const { command, detail = false } = Astro.props;
+---
+
+
+ {command.text || "Install command unavailable"}
+ {
+ detail && command.copyable ? (
+
+ ) : detail ? (
+ Copy unavailable
+ ) : (
+ Preview
+ )
+ }
+
diff --git a/apps/catalog/src/components/ResultList.astro b/apps/catalog/src/components/ResultList.astro
new file mode 100644
index 00000000..c987c0c1
--- /dev/null
+++ b/apps/catalog/src/components/ResultList.astro
@@ -0,0 +1,112 @@
+---
+import HugeIcon from "./HugeIcon.astro";
+import type { CatalogSearchRow } from "../lib/search-row";
+import { AlertCircleIcon, catalogStatusIcons, catalogTrustIcons, Copy01Icon } from "../lib/status-icons";
+
+type Props = {
+ rows: CatalogSearchRow[];
+};
+
+const { rows } = Astro.props;
+const initialRows = rows.slice(0, 24);
+---
+
+
+
+
+
+
+ {initialRows.map((row, index) => (
+
+
+ {row.icon ? (
+

+ ) : (
+
+ {row.name.slice(0, 1).toUpperCase()}
+
+ )}
+
+
+ {row.description}
+ {row.installCountDisplay}
+
+
+
+
+ {row.statuses.map((status) => (
+
+
+ {status.label}
+
+ ))}
+
+
+ {row.installCommandCopyable ? (
+
+ ) : (
+ {row.installCommandPreview}
+ )}
+
+
+ ))}
+
+
+
+ 0}>
+
No matching Caplets
+
Reset filters or search for the service, workflow, or setup requirement you need.
+
+
+
diff --git a/apps/catalog/src/components/SafetyNotice.astro b/apps/catalog/src/components/SafetyNotice.astro
new file mode 100644
index 00000000..1e3bfa5b
--- /dev/null
+++ b/apps/catalog/src/components/SafetyNotice.astro
@@ -0,0 +1,33 @@
+---
+import type { CatalogWarning } from "@caplets/core/catalog";
+import { AlertCircleIcon } from "@hugeicons/core-free-icons";
+import HugeIcon from "./HugeIcon.astro";
+
+type Props = {
+ warnings: CatalogWarning[];
+ compact?: boolean;
+};
+
+const { compact = false, warnings } = Astro.props;
+---
+
+{
+ warnings.length > 0 && (
+
+
+
+
+
+
{compact ? "Warnings" : "Inspect before installing"}
+
+
+ {warnings.map((warning) => (
+ -
+ {warning.label}
+ {!compact && {warning.message}}
+
+ ))}
+
+
+ )
+}
diff --git a/apps/catalog/src/components/SearchShell.astro b/apps/catalog/src/components/SearchShell.astro
new file mode 100644
index 00000000..6fd5b806
--- /dev/null
+++ b/apps/catalog/src/components/SearchShell.astro
@@ -0,0 +1,53 @@
+---
+import type { CatalogEntryRecord } from "../lib/catalog-store";
+import { catalogSearchRowsFromEntries } from "../lib/search-row";
+import { catalogTrustIcons } from "../lib/status-icons";
+import FilterBar from "./FilterBar.astro";
+import ResultList from "./ResultList.astro";
+import HugeIcon from "./HugeIcon.astro";
+import {
+ AlertCircleIcon,
+ ComputerUserIcon,
+ DatabaseSyncIcon,
+ Key01Icon,
+ Link01Icon,
+ Settings02Icon,
+} from "@hugeicons/core-free-icons";
+
+type Props = {
+ entries: CatalogEntryRecord[];
+};
+
+const { entries } = Astro.props;
+const rows = catalogSearchRowsFromEntries(entries);
+const tags = [...new Set(rows.flatMap((entry) => entry.tags))].sort((left, right) =>
+ left.localeCompare(right),
+);
+const indexJson = JSON.stringify(rows).replace(/
+
+
+
+ {legend.map((item) => (
+
+
+ {item.label}
+
+ ))}
+
+
+
+
+
diff --git a/apps/catalog/src/components/ThemeInit.astro b/apps/catalog/src/components/ThemeInit.astro
new file mode 100644
index 00000000..75473db4
--- /dev/null
+++ b/apps/catalog/src/components/ThemeInit.astro
@@ -0,0 +1,3 @@
+
diff --git a/apps/catalog/src/components/ThemeToggle.astro b/apps/catalog/src/components/ThemeToggle.astro
new file mode 100644
index 00000000..7e552903
--- /dev/null
+++ b/apps/catalog/src/components/ThemeToggle.astro
@@ -0,0 +1,96 @@
+---
+import {
+ ArrowDown01Icon,
+ ComputerIcon,
+ Moon02Icon,
+ Sun01Icon,
+ Tick02Icon,
+} from "@hugeicons/core-free-icons";
+import HugeIcon from "./HugeIcon.astro";
+
+const options = [
+ { value: "light", label: "Light", icon: Sun01Icon },
+ { value: "dark", label: "Dark", icon: Moon02Icon },
+ { value: "system", label: "System", icon: ComputerIcon },
+];
+---
+
+
+
+
+
+ {
+ options.map((option) => (
+
+ ))
+ }
+
+
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/badge/Badge.astro b/apps/catalog/src/components/starwind/badge/Badge.astro
new file mode 100644
index 00000000..dcdf7a2e
--- /dev/null
+++ b/apps/catalog/src/components/starwind/badge/Badge.astro
@@ -0,0 +1,17 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import type { VariantProps } from "tailwind-variants";
+
+import { badge } from "./variants";
+
+interface Props
+ extends HTMLAttributes<"div">, Omit, "type">, VariantProps {}
+
+const { variant, size, class: className, ...rest } = Astro.props;
+const isLink = Astro.props.href ? true : false;
+const Tag = Astro.props.href ? "a" : "div";
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/badge/index.ts b/apps/catalog/src/components/starwind/badge/index.ts
new file mode 100644
index 00000000..53fcc369
--- /dev/null
+++ b/apps/catalog/src/components/starwind/badge/index.ts
@@ -0,0 +1,7 @@
+import Badge from "./Badge.astro";
+import { badge } from "./variants";
+const BadgeVariants = { badge };
+
+export { Badge, BadgeVariants };
+
+export default Badge;
diff --git a/apps/catalog/src/components/starwind/badge/variants.ts b/apps/catalog/src/components/starwind/badge/variants.ts
new file mode 100644
index 00000000..3a9358ce
--- /dev/null
+++ b/apps/catalog/src/components/starwind/badge/variants.ts
@@ -0,0 +1,41 @@
+import { tv } from "tailwind-variants";
+
+export const badge = tv({
+ base: [
+ "starwind-badge inline-flex items-center gap-1.5 rounded-full font-medium whitespace-nowrap",
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
+ "transition-all outline-none focus-visible:ring-3",
+ "aria-invalid:border-error aria-invalid:focus-visible:ring-error/40",
+ ],
+ variants: {
+ variant: {
+ default: "bg-foreground text-background focus-visible:ring-outline/50",
+ primary: "bg-primary text-primary-foreground focus-visible:ring-primary/50",
+ secondary: "bg-secondary text-secondary-foreground focus-visible:ring-secondary/50",
+ outline: "border-border focus-visible:border-outline focus-visible:ring-outline/50 border",
+ ghost: "bg-foreground/10 text-foreground focus-visible:ring-outline/50",
+ info: "bg-info text-info-foreground focus-visible:ring-info/50",
+ success: "bg-success text-success-foreground focus-visible:ring-success/50",
+ warning: "bg-warning text-warning-foreground focus-visible:ring-warning/50",
+ error: "bg-error text-error-foreground focus-visible:ring-error/50",
+ },
+ size: {
+ sm: "px-2.5 py-0.5 text-xs [&_svg:not([class*='size-'])]:size-3",
+ md: "px-3 py-0.5 text-sm [&_svg:not([class*='size-'])]:size-4",
+ lg: "px-4 py-1 text-base [&_svg:not([class*='size-'])]:size-4.5",
+ },
+ isLink: { true: "cursor-pointer", false: "" },
+ },
+ compoundVariants: [
+ { isLink: true, variant: "default", className: "hover:bg-foreground/80" },
+ { isLink: true, variant: "primary", className: "hover:bg-primary/80" },
+ { isLink: true, variant: "secondary", className: "hover:bg-secondary/80" },
+ { isLink: true, variant: "outline", className: "hover:border-border/80" },
+ { isLink: true, variant: "ghost", className: "hover:bg-foreground/7" },
+ { isLink: true, variant: "info", className: "hover:bg-info/80" },
+ { isLink: true, variant: "success", className: "hover:bg-success/80" },
+ { isLink: true, variant: "warning", className: "hover:bg-warning/80" },
+ { isLink: true, variant: "error", className: "hover:bg-error/80" },
+ ],
+ defaultVariants: { variant: "default", size: "md", isLink: false },
+});
diff --git a/apps/catalog/src/components/starwind/button/Button.astro b/apps/catalog/src/components/starwind/button/Button.astro
new file mode 100644
index 00000000..b1e00cd0
--- /dev/null
+++ b/apps/catalog/src/components/starwind/button/Button.astro
@@ -0,0 +1,26 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import type { VariantProps } from "tailwind-variants";
+
+import { button } from "./variants";
+
+interface Props
+ extends
+ HTMLAttributes<"button">,
+ Omit, "type">,
+ VariantProps {}
+
+const { variant, size, class: className, type, ...rest } = Astro.props;
+
+const isLink = Boolean(Astro.props.href);
+const Tag = isLink ? "a" : "button";
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/button/index.ts b/apps/catalog/src/components/starwind/button/index.ts
new file mode 100644
index 00000000..16de9b64
--- /dev/null
+++ b/apps/catalog/src/components/starwind/button/index.ts
@@ -0,0 +1,7 @@
+import Button from "./Button.astro";
+import { button } from "./variants";
+const ButtonVariants = { button };
+
+export { Button, ButtonVariants };
+
+export default Button;
diff --git a/apps/catalog/src/components/starwind/button/variants.ts b/apps/catalog/src/components/starwind/button/variants.ts
new file mode 100644
index 00000000..23e8b831
--- /dev/null
+++ b/apps/catalog/src/components/starwind/button/variants.ts
@@ -0,0 +1,38 @@
+import { tv } from "tailwind-variants";
+
+export const button = tv({
+ base: [
+ "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap",
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
+ "transition-all outline-none focus-visible:ring-3",
+ "disabled:pointer-events-none disabled:opacity-50",
+ "aria-invalid:border-error aria-invalid:focus-visible:ring-error/40",
+ ],
+ variants: {
+ variant: {
+ default: "bg-foreground text-background hover:bg-foreground/90 focus-visible:ring-outline/50",
+ primary:
+ "bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/90 focus-visible:ring-secondary/50",
+ outline:
+ "dark:border-input focus-visible:ring-outline/50 bg-background dark:bg-input/30 focus-visible:border-outline hover:bg-muted dark:hover:bg-input/50 hover:text-foreground border shadow-xs",
+ ghost: "hover:bg-muted hover:text-foreground focus-visible:ring-outline/50",
+ info: "bg-info text-info-foreground hover:bg-info/90 focus-visible:ring-info/50",
+ success:
+ "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/50",
+ warning:
+ "bg-warning text-warning-foreground hover:bg-warning/90 focus-visible:ring-warning/50",
+ error: "bg-error text-error-foreground hover:bg-error/90 focus-visible:ring-error/50",
+ },
+ size: {
+ sm: "h-9 px-4 text-sm has-[>svg]:px-3 [&_svg:not([class*='size-'])]:size-3.5",
+ md: "h-11 px-5 text-base has-[>svg]:px-4 [&_svg:not([class*='size-'])]:size-4.5",
+ lg: "h-12 px-8 text-lg has-[>svg]:px-6 [&_svg:not([class*='size-'])]:size-5",
+ "icon-sm": "size-9 [&_svg:not([class*='size-'])]:size-3.5",
+ icon: "size-11 [&_svg:not([class*='size-'])]:size-4.5",
+ "icon-lg": "size-12 [&_svg:not([class*='size-'])]:size-5",
+ },
+ },
+ defaultVariants: { variant: "default", size: "md" },
+});
diff --git a/apps/catalog/src/components/starwind/card/Card.astro b/apps/catalog/src/components/starwind/card/Card.astro
new file mode 100644
index 00000000..8af25541
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/Card.astro
@@ -0,0 +1,19 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import type { VariantProps } from "tailwind-variants";
+
+import { card } from "./variants";
+
+type Props = HTMLAttributes<"div"> & VariantProps;
+
+const { class: className, size, ...rest } = Astro.props;
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardAction.astro b/apps/catalog/src/components/starwind/card/CardAction.astro
new file mode 100644
index 00000000..17965904
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardAction.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardAction } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardContent.astro b/apps/catalog/src/components/starwind/card/CardContent.astro
new file mode 100644
index 00000000..365e809c
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardContent.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardContent } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardDescription.astro b/apps/catalog/src/components/starwind/card/CardDescription.astro
new file mode 100644
index 00000000..8b5079b3
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardDescription.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardDescription } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardFooter.astro b/apps/catalog/src/components/starwind/card/CardFooter.astro
new file mode 100644
index 00000000..1ba4323e
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardFooter.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardFooter } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardHeader.astro b/apps/catalog/src/components/starwind/card/CardHeader.astro
new file mode 100644
index 00000000..011353be
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardHeader.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardHeader } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
diff --git a/apps/catalog/src/components/starwind/card/CardTitle.astro b/apps/catalog/src/components/starwind/card/CardTitle.astro
new file mode 100644
index 00000000..25f74e97
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/CardTitle.astro
@@ -0,0 +1,13 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+import { cardTitle } from "./variants";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+
+
+
diff --git a/apps/catalog/src/components/starwind/card/index.ts b/apps/catalog/src/components/starwind/card/index.ts
new file mode 100644
index 00000000..747f5313
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/index.ts
@@ -0,0 +1,46 @@
+import Card from "./Card.astro";
+import CardAction from "./CardAction.astro";
+import CardContent from "./CardContent.astro";
+import CardDescription from "./CardDescription.astro";
+import CardFooter from "./CardFooter.astro";
+import CardHeader from "./CardHeader.astro";
+import CardTitle from "./CardTitle.astro";
+import {
+ card,
+ cardAction,
+ cardContent,
+ cardDescription,
+ cardFooter,
+ cardHeader,
+ cardTitle,
+} from "./variants";
+const CardVariants = {
+ card,
+ cardAction,
+ cardContent,
+ cardDescription,
+ cardFooter,
+ cardHeader,
+ cardTitle,
+};
+
+export {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ CardVariants,
+};
+
+export default {
+ Root: Card,
+ Header: CardHeader,
+ Footer: CardFooter,
+ Title: CardTitle,
+ Description: CardDescription,
+ Content: CardContent,
+ Action: CardAction,
+};
diff --git a/apps/catalog/src/components/starwind/card/variants.ts b/apps/catalog/src/components/starwind/card/variants.ts
new file mode 100644
index 00000000..5c803154
--- /dev/null
+++ b/apps/catalog/src/components/starwind/card/variants.ts
@@ -0,0 +1,45 @@
+import { tv } from "tailwind-variants";
+
+export const card = tv({
+ base: [
+ "bg-card text-card-foreground group/card ring-border flex flex-col rounded-xl ring-1",
+ "has-[[data-slot=card-footer]]:pb-0 has-[>img:first-child]:pt-0",
+ "*:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ ],
+ variants: {
+ size: {
+ default: "gap-6 py-6",
+ sm: "gap-4 py-4 text-sm",
+ },
+ },
+ defaultVariants: {
+ size: "default",
+ },
+});
+
+export const cardAction = tv({
+ base: "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+});
+
+export const cardContent = tv({
+ base: "px-6 group-data-[size=sm]/card:px-4",
+});
+
+export const cardDescription = tv({
+ base: "text-muted-foreground text-base group-data-[size=sm]/card:text-sm",
+});
+
+export const cardFooter = tv({
+ base: "bg-muted/50 flex items-center rounded-b-xl border-t p-6 group-data-[size=sm]/card:p-4",
+});
+
+export const cardHeader = tv({
+ base: [
+ "@container/card-header grid auto-rows-min items-start gap-1 px-6 group-data-[size=sm]/card:px-4",
+ "has-[[data-slot=card-action]]:grid-cols-[1fr_auto] has-[[data-slot=card-description]]:grid-rows-[auto_auto]",
+ ],
+});
+
+export const cardTitle = tv({
+ base: "font-heading text-xl leading-snug font-medium group-data-[size=sm]/card:text-base",
+});
diff --git a/apps/catalog/src/components/starwind/select/Select.astro b/apps/catalog/src/components/starwind/select/Select.astro
new file mode 100644
index 00000000..ea3090cd
--- /dev/null
+++ b/apps/catalog/src/components/starwind/select/Select.astro
@@ -0,0 +1,757 @@
+---
+import type { HTMLAttributes } from "astro/types";
+
+type Props = HTMLAttributes<"div"> & {
+ /**
+ * The name for the hidden