diff options
Diffstat (limited to 'website/src')
| -rw-r--r-- | website/src/components/MarketingLayout.astro | 6 | ||||
| -rw-r--r-- | website/src/components/SiteTitle.astro | 2 | ||||
| -rw-r--r-- | website/src/content.config.ts | 11 | ||||
| -rw-r--r-- | website/src/content/blog/herdr-0-6-3.md (renamed from website/src/content/releases/v0-6-3.md) | 5 | ||||
| -rw-r--r-- | website/src/content/docs/marketplace.mdx | 54 | ||||
| -rw-r--r-- | website/src/content/docs/plugins.mdx | 15 | ||||
| -rw-r--r-- | website/src/data/plugins-fixture.json | 139 | ||||
| -rw-r--r-- | website/src/pages/compare.astro | 6 | ||||
| -rw-r--r-- | website/src/pages/plugins.astro | 601 | ||||
| -rw-r--r-- | website/src/pages/releases/[...slug].astro | 89 | ||||
| -rw-r--r-- | website/src/pages/releases/index.astro | 42 |
11 files changed, 791 insertions, 179 deletions
diff --git a/website/src/components/MarketingLayout.astro b/website/src/components/MarketingLayout.astro index a4df022f..381daf03 100644 --- a/website/src/components/MarketingLayout.astro +++ b/website/src/components/MarketingLayout.astro @@ -58,7 +58,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString(); document.documentElement.setAttribute("data-palette", saved || system); })(); </script> - <link rel="stylesheet" href="/css/style.css?v=21" /> + <link rel="stylesheet" href="/css/style.css?v=39" /> </head> <body> <header class="site-header"> @@ -76,7 +76,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString(); <div class="nav-menu-panel"> <a href="/">Home</a> <a href="/compare/">Compare</a> - <a href="/releases/">Releases</a> + <a href="/plugins/">Plugins</a> <a href="/blog/">Blog</a> <a href="/docs/quick-start/">Quick start</a> <a href="/docs/">Docs</a> @@ -88,7 +88,7 @@ const ogImageUrl = new URL(ogImage, 'https://herdr.dev').toString(); <div class="nav-links"> <a href="/">Home</a> <a href="/compare/">Compare</a> - <a href="/releases/">Releases</a> + <a href="/plugins/">Plugins</a> <a href="/blog/">Blog</a> <a href="/docs/quick-start/">Quick start</a> <a href="/docs/">Docs</a> diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro index d35da5d6..f6a3b7d0 100644 --- a/website/src/components/SiteTitle.astro +++ b/website/src/components/SiteTitle.astro @@ -11,7 +11,7 @@ const docsHref = isPreview ? '/docs/preview/' : '/docs/'; </a> <nav class="title-nav" aria-label="Primary"> <a href={docsHref}>Docs</a> - <a href="/releases/">Releases</a> + <a href="/plugins/">Plugins</a> <a href="/blog/">Blog</a> </nav> </div> diff --git a/website/src/content.config.ts b/website/src/content.config.ts index ba0c83f8..bb0f2d8a 100644 --- a/website/src/content.config.ts +++ b/website/src/content.config.ts @@ -21,15 +21,4 @@ export const collections = { ogImage: z.string().optional(), }), }), - releases: defineCollection({ - loader: glob({ pattern: '*.md', base: './src/content/releases' }), - schema: z.object({ - title: z.string(), - version: z.string(), - path: z.string(), - description: z.string(), - date: z.coerce.date(), - draft: z.boolean().default(false), - }), - }), }; diff --git a/website/src/content/releases/v0-6-3.md b/website/src/content/blog/herdr-0-6-3.md index 23514cc6..97bd2b28 100644 --- a/website/src/content/releases/v0-6-3.md +++ b/website/src/content/blog/herdr-0-6-3.md @@ -1,9 +1,8 @@ --- -title: "session persistence, safer updates, and workspace navigation" -version: "0.6.3" -path: "0.6.3/session-persistence-safer-updates-and-workspace-navigation" +title: "Herdr 0.6.3: session persistence, safer updates, and workspace navigation" description: "This one adds new persistence paths for updates, server restarts, and larger sessions." date: 2026-05-27 +draft: false --- ## Persistence for the hard parts diff --git a/website/src/content/docs/marketplace.mdx b/website/src/content/docs/marketplace.mdx index f07b8f87..09a27b2d 100644 --- a/website/src/content/docs/marketplace.mdx +++ b/website/src/content/docs/marketplace.mdx @@ -1,39 +1,53 @@ --- title: Marketplace -description: Publish and share Herdr plugins, and get listed when the marketplace launches. +description: Discover community Herdr plugins on GitHub, and get your own plugin listed. --- -The Herdr plugin marketplace is a discoverable index of installable plugins. It -is not live yet. This page is how to be ready for it. +The Herdr plugin marketplace is a discoverable index of community plugins. +Browse it at [herdr.dev/plugins](/plugins/). It is an automatic index of public +GitHub repositories, not a reviewed catalog. -## Share a plugin today +## Browse plugins + +The [marketplace](/plugins/) lists every public repository tagged with the +GitHub topic `herdr-plugin`. Search by name, owner, description, or language, +and sort by popularity, recent activity, or newest. Each listing links straight +to its source repository on GitHub. + +Discovery is automatic and unreviewed. A listing means a repository tagged +itself, not that Herdr vetted it, so the +[trust guidance](/docs/plugins/#trust-and-security) applies before you install +anything. + +## Install a plugin The marketplace adds discovery on top of installation; it does not replace it. -Anyone can already install your plugin straight from GitHub: +Install any plugin straight from GitHub: ```bash herdr plugin install owner/repo[/subdir...] ``` Publish a normal public GitHub repository with a `herdr-plugin.toml` manifest at -its root, or in a subdirectory, and that command works now. See +its root, or in a subdirectory, and that command works. See [Plugins](/docs/plugins/) for the manifest and authoring reference. -## Get listed when it launches +## Get your plugin listed -If you want your plugin to appear in the website marketplace when discovery -launches, add the GitHub topic `herdr-plugin` to your repository now. That -topic is the signal the future index will use for discovery, so tagging a -public plugin today means it can be listed when the marketplace goes live. +Add the GitHub topic `herdr-plugin` to a public repository. That topic is the +only signal the index uses, so tagging a public plugin is all it takes. The +index refreshes automatically every 30 minutes, so a newly tagged repository +shows up shortly after, and one that drops the topic disappears on the next +refresh. -## What a listing will show +## What a listing shows -A marketplace card will surface your plugin's `id`, `name`, `description`, -declared `platforms`, and a link back to the source repository. Getting that -manifest metadata right today is what makes your listing accurate later. Declare -`platforms` honestly, because the index will use it to show where a plugin runs. +Each card shows GitHub repository metadata: the repository name and owner, its +description, star count, primary language, and the time it was last pushed, with +a link back to the source. The index reads this from GitHub's repository search, +so keeping your repository description and topics accurate is what makes your +listing useful. -Plugins stay ordinary GitHub repositories. The marketplace is a discovery layer -on top of the same `herdr plugin install` path you can use now. A listing makes a -plugin easier to find; it does not mean Herdr has reviewed it, so the -[trust guidance](/docs/plugins/#trust-and-security) still applies. +The index does not parse `herdr-plugin.toml` yet, so manifest fields like the +plugin `id`, declared `platforms`, and `min_herdr_version` are not shown in v1. +Forks and archived repositories are excluded from the list. diff --git a/website/src/content/docs/plugins.mdx b/website/src/content/docs/plugins.mdx index 9f069fdf..9f57ff47 100644 --- a/website/src/content/docs/plugins.mdx +++ b/website/src/content/docs/plugins.mdx @@ -304,10 +304,11 @@ state should own their files or database. ## Marketplace -A plugin marketplace is coming. Plugins are already shareable today: publish a -GitHub repository with `herdr-plugin.toml`, then share -`herdr plugin install owner/repo[/subdir]`. - -If you want the plugin to appear in the website marketplace when discovery -launches, add the GitHub topic `herdr-plugin` to the repository now. See -[Marketplace](/docs/marketplace/) for how publishing and discovery will work. +Community plugins are discoverable in the [marketplace](/plugins/), an automatic +index of public GitHub repositories tagged with the topic `herdr-plugin`. +Plugins stay ordinary GitHub repositories: publish one with `herdr-plugin.toml`, +then share `herdr plugin install owner/repo[/subdir]`. + +To get a plugin listed, add the GitHub topic `herdr-plugin` to its public +repository. The index refreshes every 30 minutes. See +[Marketplace](/docs/marketplace/) for how discovery works. diff --git a/website/src/data/plugins-fixture.json b/website/src/data/plugins-fixture.json new file mode 100644 index 00000000..27a9c265 --- /dev/null +++ b/website/src/data/plugins-fixture.json @@ -0,0 +1,139 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-06-20T12:00:00.000Z", + "source": { + "provider": "github", + "query": "topic:herdr-plugin", + "totalCount": 7, + "collectedCount": 7, + "truncated": false + }, + "plugins": [ + { + "id": 123456789, + "fullName": "ogulcancelik/herdr-plugin-github-start", + "owner": "ogulcancelik", + "name": "herdr-plugin-github-start", + "description": "Start Codex or Claude from a GitHub issue, PR, or discussion without leaving the terminal.", + "url": "https://github.com/ogulcancelik/herdr-plugin-github-start", + "stars": 42, + "forks": 3, + "openIssues": 1, + "language": "TypeScript", + "topics": ["herdr-plugin", "github", "agents"], + "createdAt": "2026-05-01T00:00:00Z", + "updatedAt": "2026-06-19T19:58:26Z", + "pushedAt": "2026-06-19T19:58:26Z", + "archived": false, + "fork": false + }, + { + "id": 223456789, + "fullName": "ogulcancelik/herdr-plugin-examples", + "owner": "ogulcancelik", + "name": "herdr-plugin-examples", + "description": "Cookbook of example Herdr plugins: telegram notify, link preview, dev layout bootstrap.", + "url": "https://github.com/ogulcancelik/herdr-plugin-examples", + "stars": 28, + "forks": 6, + "openIssues": 0, + "language": "JavaScript", + "topics": ["herdr-plugin", "examples"], + "createdAt": "2026-04-12T00:00:00Z", + "updatedAt": "2026-06-18T08:10:00Z", + "pushedAt": "2026-06-18T08:10:00Z", + "archived": false, + "fork": false + }, + { + "id": 323456789, + "fullName": "lindqvist/herdr-board", + "owner": "lindqvist", + "name": "herdr-board", + "description": "A live project board pane for Herdr that tracks agent tasks across worktrees.", + "url": "https://github.com/lindqvist/herdr-board", + "stars": 19, + "forks": 1, + "openIssues": 4, + "language": "Rust", + "topics": ["herdr-plugin", "pane", "kanban"], + "createdAt": "2026-05-20T00:00:00Z", + "updatedAt": "2026-06-17T14:02:00Z", + "pushedAt": "2026-06-17T14:02:00Z", + "archived": false, + "fork": false + }, + { + "id": 423456789, + "fullName": "mira-dev/herdr-layout-presets", + "owner": "mira-dev", + "name": "herdr-layout-presets", + "description": "Apply opinionated split-pane layouts per project with one keybinding.", + "url": "https://github.com/mira-dev/herdr-layout-presets", + "stars": 11, + "forks": 0, + "openIssues": 2, + "language": "Lua", + "topics": ["herdr-plugin", "layout"], + "createdAt": "2026-06-01T00:00:00Z", + "updatedAt": "2026-06-15T09:30:00Z", + "pushedAt": "2026-06-15T09:30:00Z", + "archived": false, + "fork": false + }, + { + "id": 523456789, + "fullName": "okabe/herdr-jira-bridge", + "owner": "okabe", + "name": "herdr-jira-bridge", + "description": "Open Jira tickets as Herdr workspaces and push agent summaries back as comments.", + "url": "https://github.com/okabe/herdr-jira-bridge", + "stars": 7, + "forks": 2, + "openIssues": 0, + "language": "Python", + "topics": ["herdr-plugin", "jira", "integration"], + "createdAt": "2026-05-28T00:00:00Z", + "updatedAt": "2026-06-10T11:45:00Z", + "pushedAt": "2026-06-10T11:45:00Z", + "archived": false, + "fork": false + }, + { + "id": 623456789, + "fullName": "noor/herdr-notify", + "owner": "noor", + "name": "herdr-notify", + "description": "Desktop and mobile notifications when an agent finishes, blocks, or needs input.", + "url": "https://github.com/noor/herdr-notify", + "stars": 4, + "forks": 0, + "openIssues": 1, + "language": "Go", + "topics": ["herdr-plugin", "notifications"], + "createdAt": "2026-06-05T00:00:00Z", + "updatedAt": "2026-06-08T16:20:00Z", + "pushedAt": "2026-06-08T16:20:00Z", + "archived": false, + "fork": false + }, + { + "id": 723456789, + "fullName": "small-tools/herdr-snippet-runner", + "owner": "small-tools", + "name": "herdr-snippet-runner", + "description": null, + "url": "https://github.com/small-tools/herdr-snippet-runner", + "stars": 0, + "forks": 0, + "openIssues": 0, + "language": null, + "topics": ["herdr-plugin"], + "createdAt": "2026-06-14T00:00:00Z", + "updatedAt": "2026-06-14T10:00:00Z", + "pushedAt": "2026-06-14T10:00:00Z", + "archived": false, + "fork": false + } + ] +} diff --git a/website/src/pages/compare.astro b/website/src/pages/compare.astro index 0825ea68..f120ac59 100644 --- a/website/src/pages/compare.astro +++ b/website/src/pages/compare.astro @@ -45,7 +45,7 @@ document.documentElement.setAttribute("data-palette", saved || system); })(); </script> - <link rel="stylesheet" href="/css/style.css?v=21" /> + <link rel="stylesheet" href="/css/style.css?v=39" /> </head> <body> <header class="site-header"> @@ -63,7 +63,7 @@ <div class="nav-menu-panel"> <a href="/">Home</a> <a href="/compare/" aria-current="page">Compare</a> - <a href="/releases/">Releases</a> + <a href="/plugins/">Plugins</a> <a href="/blog/">Blog</a> <a href="/docs/quick-start/">Quick start</a> <a href="/docs/">Docs</a> @@ -75,7 +75,7 @@ <div class="nav-links"> <a href="/">Home</a> <a href="/compare/" aria-current="page">Compare</a> - <a href="/releases/">Releases</a> + <a href="/plugins/">Plugins</a> <a href="/blog/">Blog</a> <a href="/docs/quick-start/">Quick start</a> <a href="/docs/">Docs</a> diff --git a/website/src/pages/plugins.astro b/website/src/pages/plugins.astro new file mode 100644 index 00000000..6e96a132 --- /dev/null +++ b/website/src/pages/plugins.astro @@ -0,0 +1,601 @@ +--- +import MarketingLayout from '../components/MarketingLayout.astro'; +import fixture from '../data/plugins-fixture.json'; + +const snapshotUrl = 'https://assets.herdr.dev/plugins/index.json'; + +// Seed the page server-side from the public R2 snapshot so `bun run dev` and +// the production build both render real data immediately. If the live fetch +// fails, dev falls back to a local fixture for offline design; production seeds +// empty and lets the client fetch fill it (never fake data). +let liveData = null; +try { + const res = await fetch(snapshotUrl); + if (res.ok) liveData = await res.json(); +} catch { + // ignore: handled by the fallbacks below +} +const initialData = liveData ?? (import.meta.env.DEV ? fixture : null); +const initialIsFixture = !liveData && import.meta.env.DEV; +const apiUrl = import.meta.env.DEV ? null : snapshotUrl; +--- + +<MarketingLayout + title="Herdr plugins: community plugin marketplace" + description="Discover community Herdr plugins published on GitHub. An automatic index of public repositories tagged with the herdr-plugin topic." + canonicalPath="/plugins/" +> + <main class="plugins-page"> + <section class="section plugins-hero"> + <p class="section-kicker"># marketplace</p> + <h2>Plugins built on Herdr.</h2> + <p class="plugins-lede"> + Community plugins published on GitHub. Install any of them with + <code>herdr plugin install</code>, or publish your own and get listed. + </p> + <p class="plugins-trust"> + <span class="status-dot dot-idle"></span> + Listings are discovered automatically from the GitHub topic + <code>herdr-plugin</code>. They are not reviewed by Herdr, so install at + your own discretion. <a href="/docs/plugins/#trust-and-security">Trust guidance</a>. + </p> + </section> + + <section class="section plugins-controls"> + <div class="plugins-search-wrap"> + <span class="plugins-search-icon" aria-hidden="true">/</span> + <input + id="plugins-search" + type="search" + class="plugins-search" + placeholder="search plugins, owners, languages…" + autocomplete="off" + spellcheck="false" + aria-label="Search plugins" + /> + </div> + <label class="plugins-sort-wrap"> + <span class="plugins-sort-label">sort</span> + <select id="plugins-sort" class="plugins-sort" aria-label="Sort plugins"> + <option value="popular">Popular</option> + <option value="recent">Recently updated</option> + <option value="newest">Newest</option> + <option value="name">Name</option> + </select> + </label> + <p id="plugins-meta" class="plugins-meta" aria-live="polite"></p> + </section> + + <section class="section plugins-results"> + <div id="plugins-grid" class="plugins-grid" role="list"></div> + <div id="plugins-status" class="plugins-status">Loading plugins…</div> + </section> + + <section class="section plugins-publish"> + <div class="plugins-publish-card"> + <p class="section-kicker"># publish</p> + <h3>Want your plugin listed?</h3> + <p> + Add the GitHub topic <code>herdr-plugin</code> to a public repository + with a <code>herdr-plugin.toml</code> manifest. The index picks it up + on the next refresh. No submission, no review queue. + </p> + <div class="plugins-publish-actions"> + <a class="button button-primary" href="/docs/marketplace/">How publishing works</a> + <a class="button button-secondary" href="/docs/plugins/">Author a plugin</a> + </div> + </div> + </section> + </main> + + <script define:vars={{ initialData, initialIsFixture, apiUrl }}> + const grid = document.getElementById("plugins-grid"); + const statusEl = document.getElementById("plugins-status"); + const searchInput = document.getElementById("plugins-search"); + const sortSelect = document.getElementById("plugins-sort"); + const metaEl = document.getElementById("plugins-meta"); + + const LANG_COLORS = { + TypeScript: "#3178c6", + JavaScript: "#f1e05a", + Rust: "#dea584", + Python: "#3572A5", + Go: "#00ADD8", + Lua: "#000080", + Shell: "#89e051", + "C++": "#f34b7d", + Ruby: "#701516", + }; + + let plugins = []; + let usingFallback = false; + + function timeAgo(iso) { + if (!iso) return null; + const then = new Date(iso).getTime(); + if (Number.isNaN(then)) return null; + const secs = Math.max(0, Math.round((Date.now() - then) / 1000)); + const units = [ + ["y", 31536000], + ["mo", 2592000], + ["w", 604800], + ["d", 86400], + ["h", 3600], + ["m", 60], + ]; + for (const [label, size] of units) { + const v = Math.floor(secs / size); + if (v >= 1) return v + label + " ago"; + } + return "just now"; + } + + function isGithubUrl(url) { + return typeof url === "string" && /^https:\/\/github\.com\/[^/]+\/[^/]+/.test(url); + } + + function el(tag, className, text) { + const node = document.createElement(tag); + if (className) node.className = className; + if (text != null) node.textContent = text; + return node; + } + + function buildCard(p) { + const url = isGithubUrl(p.url) ? p.url : null; + const card = el(url ? "a" : "div", "plugin-card"); + card.setAttribute("role", "listitem"); + if (url) { + card.href = url; + card.target = "_blank"; + card.rel = "noopener noreferrer"; + } + + const head = el("div", "plugin-card-head"); + const title = el("div", "plugin-title"); + title.appendChild(el("span", "plugin-owner", (p.owner || "?") + "/")); + title.appendChild(el("span", "plugin-name", p.name || "unknown")); + head.appendChild(title); + head.appendChild(el("span", "plugin-go", "↗")); + card.appendChild(head); + + const desc = el( + "p", + "plugin-desc" + (p.description ? "" : " is-empty"), + p.description || "No description provided.", + ); + card.appendChild(desc); + + const meta = el("div", "plugin-meta-row"); + + const stars = el("span", "plugin-stat"); + stars.appendChild(el("span", "plugin-star", "★")); + stars.appendChild(el("span", null, String(p.stars ?? 0))); + meta.appendChild(stars); + + if (p.language) { + const lang = el("span", "plugin-stat"); + const dot = el("span", "plugin-lang-dot"); + dot.style.background = LANG_COLORS[p.language] || "var(--muted)"; + lang.appendChild(dot); + lang.appendChild(el("span", null, p.language)); + meta.appendChild(lang); + } + + const updated = timeAgo(p.pushedAt); + if (updated) meta.appendChild(el("span", "plugin-stat plugin-updated", "updated " + updated)); + + card.appendChild(meta); + return card; + } + + function matches(p, q) { + if (!q) return true; + const hay = [p.fullName, p.owner, p.name, p.description, p.language, ...(p.topics || [])] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return q.split(/\s+/).every((term) => hay.includes(term)); + } + + function sortPlugins(list, mode) { + const by = [...list]; + const t = (v) => new Date(v || 0).getTime() || 0; + if (mode === "recent") by.sort((a, b) => t(b.pushedAt) - t(a.pushedAt)); + else if (mode === "newest") by.sort((a, b) => t(b.createdAt) - t(a.createdAt)); + else if (mode === "name") by.sort((a, b) => (a.fullName || "").localeCompare(b.fullName || "")); + else + by.sort( + (a, b) => (b.stars ?? 0) - (a.stars ?? 0) || t(b.pushedAt) - t(a.pushedAt), + ); + return by; + } + + function render() { + const q = searchInput.value.trim().toLowerCase(); + const visible = sortPlugins(plugins.filter((p) => matches(p, q)), sortSelect.value); + + grid.replaceChildren(); + for (const p of visible) grid.appendChild(buildCard(p)); + + if (visible.length === 0) { + statusEl.textContent = q + ? "No plugins match your search." + : "No plugins are listed yet."; + statusEl.hidden = false; + } else { + statusEl.hidden = true; + } + } + + function setMeta(generatedAt) { + const parts = []; + parts.push(plugins.length + (plugins.length === 1 ? " plugin" : " plugins")); + const ago = timeAgo(generatedAt); + if (ago) parts.push("updated " + ago); + if (usingFallback) parts.push("preview data"); + metaEl.textContent = parts.join(" · "); + } + + function apply(data) { + plugins = Array.isArray(data && data.plugins) ? data.plugins : []; + setMeta(data && data.generatedAt); + render(); + } + + function showError() { + plugins = []; + metaEl.textContent = ""; + grid.replaceChildren(); + statusEl.textContent = + "The plugin marketplace is warming up. Check back in a few minutes."; + statusEl.hidden = false; + } + + async function load() { + // Render the server-seeded data immediately (live in prod and dev; fixture + // only as a dev offline fallback). + if (initialData) { + usingFallback = initialIsFixture; + apply(initialData); + } + // Refresh from the same-origin function in production for the freshest snapshot. + if (apiUrl) { + try { + const res = await fetch(apiUrl, { headers: { accept: "application/json" } }); + if (!res.ok) throw new Error("status " + res.status); + usingFallback = false; + apply(await res.json()); + } catch (err) { + if (!initialData) showError(); + } + } else if (!initialData) { + showError(); + } + } + + searchInput.addEventListener("input", render); + sortSelect.addEventListener("change", render); + load(); + </script> + + <style is:global> + .plugins-page { + padding-bottom: 5rem; + } + + /* Reuse .section only for max-width centering, not its 5rem padding + top divider. */ + .plugins-page .section { + padding-block: 0; + border-top: none; + } + + .plugins-page .plugins-hero { + padding-top: 3rem; + } + + .plugins-hero h2 { + margin: 0; + color: var(--ink); + font-family: var(--mono); + font-weight: 400; + letter-spacing: -0.04em; + line-height: 1.05; + font-size: clamp(2rem, 4vw, 3rem); + } + + .plugins-lede { + max-width: 640px; + margin: 1.1rem 0 0; + color: var(--ink-soft); + font-size: clamp(1rem, 1.6vw, 1.15rem); + line-height: 1.6; + } + + .plugins-lede code, + .plugins-trust code, + .plugins-publish-card code { + font-family: var(--mono); + font-size: 0.85em; + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 0.1em 0.35em; + white-space: nowrap; + } + + .plugins-trust { + max-width: 640px; + margin: 1.1rem 0 0; + color: var(--muted); + font-size: 0.85rem; + line-height: 1.55; + } + + .plugins-trust .status-dot { + display: inline-block; + margin-right: 0.4rem; + vertical-align: middle; + } + + .plugins-trust a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + } + + /* Controls */ + .plugins-page .plugins-controls { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-top: 2.5rem; + padding-bottom: 1.75rem; + } + + /* Divider bound to the content box (padding box), so it aligns with the + search row and the cards instead of bleeding past them. */ + .plugins-controls::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + border-top: 1px solid var(--line); + } + + .plugins-search-wrap { + position: relative; + flex: 1 1 18rem; + display: flex; + align-items: center; + } + + .plugins-search-icon { + position: absolute; + left: 0.7rem; + color: var(--muted-2); + font-family: var(--mono); + font-size: 0.9rem; + pointer-events: none; + } + + .plugins-search { + width: 100%; + padding: 0.6rem 0.8rem 0.6rem 1.7rem; + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius-md); + color: var(--ink); + font-family: var(--mono); + font-size: 0.85rem; + } + + .plugins-search:focus { + outline: none; + border-color: var(--accent); + } + + .plugins-sort-wrap { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .plugins-sort-label { + color: var(--muted); + font-family: var(--mono); + font-size: 0.78rem; + text-transform: lowercase; + letter-spacing: 0.06em; + } + + .plugins-sort { + padding: 0.55rem 0.7rem; + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius-md); + color: var(--ink); + font-family: var(--mono); + font-size: 0.82rem; + cursor: pointer; + } + + .plugins-sort:focus { + outline: none; + border-color: var(--accent); + } + + .plugins-meta { + margin: 0; + margin-left: auto; + color: var(--muted); + font-family: var(--mono); + font-size: 0.78rem; + letter-spacing: 0.04em; + } + + /* Grid */ + .plugins-results { + margin-top: 2rem; + } + + .plugins-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + gap: 0.85rem; + } + + .plugin-card { + display: flex; + flex-direction: column; + gap: 0.7rem; + padding: 1.25rem; + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius-md); + transition: border-color 0.12s ease, transform 0.12s ease; + min-height: 8.5rem; + } + + a.plugin-card:hover { + border-color: var(--accent); + transform: translateY(-2px); + } + + .plugin-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.5rem; + } + + .plugin-title { + font-family: var(--mono); + font-size: 0.95rem; + line-height: 1.3; + word-break: break-word; + } + + .plugin-owner { + color: var(--muted); + } + + .plugin-name { + color: var(--ink); + font-weight: 600; + } + + .plugin-go { + color: var(--muted-2); + font-family: var(--mono); + flex: 0 0 auto; + transition: color 0.12s ease; + } + + a.plugin-card:hover .plugin-go { + color: var(--accent); + } + + .plugin-desc { + margin: 0; + color: var(--ink-soft); + font-size: 0.88rem; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .plugin-desc.is-empty { + color: var(--muted-2); + font-style: italic; + } + + .plugin-meta-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.9rem; + margin-top: auto; + color: var(--muted); + font-family: var(--mono); + font-size: 0.78rem; + } + + .plugin-stat { + display: inline-flex; + align-items: center; + gap: 0.35rem; + } + + .plugin-star { + color: var(--yellow, #d4a72c); + } + + .plugin-lang-dot { + width: 0.6rem; + height: 0.6rem; + border-radius: 50%; + flex: 0 0 auto; + } + + .plugin-updated { + margin-left: auto; + color: var(--muted-2); + } + + .plugins-status { + padding: 3rem 1rem; + text-align: center; + color: var(--muted); + font-family: var(--mono); + font-size: 0.85rem; + } + + /* Publish */ + .plugins-publish { + margin-top: 3.5rem; + } + + .plugins-publish-card { + padding: 2rem; + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + } + + .plugins-publish-card h3 { + margin: 0.25rem 0 0; + color: var(--ink); + font-family: var(--mono); + font-weight: 400; + font-size: 1.4rem; + letter-spacing: -0.02em; + } + + .plugins-publish-card p { + max-width: 620px; + margin: 0.8rem 0 0; + color: var(--ink-soft); + line-height: 1.6; + } + + .plugins-publish-actions { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + margin-top: 1.5rem; + } + + @media (max-width: 38rem) { + .plugins-meta { + margin-left: 0; + width: 100%; + } + } + </style> +</MarketingLayout> diff --git a/website/src/pages/releases/[...slug].astro b/website/src/pages/releases/[...slug].astro deleted file mode 100644 index 420d4b9e..00000000 --- a/website/src/pages/releases/[...slug].astro +++ /dev/null @@ -1,89 +0,0 @@ ---- -import { getCollection, render } from 'astro:content'; -import MarketingLayout from '../../components/MarketingLayout.astro'; - -export async function getStaticPaths() { - const releases = await getCollection('releases'); - return releases - .filter((release) => !release.data.draft) - .map((release) => { - const slug = release.data.path; - return { - params: { slug }, - props: { release, slug }, - }; - }); -} - -const { release, slug } = Astro.props; -const { Content, headings } = await render(release); -const sectionHeadings = headings.filter((heading) => heading.depth === 2); ---- - -<MarketingLayout - title={`${release.data.version}: ${release.data.title} — Herdr releases`} - description={release.data.description} - canonicalPath={`/releases/${slug}/`} - ogType="article" -> - <main class="content-shell"> - <div class="article-layout release-layout section"> - <aside class="article-toc" aria-label="Release sections"> - <p>Sections</p> - <nav> - {sectionHeadings.map((heading) => ( - <a href={`#${heading.slug}`} data-heading-link={heading.slug}>{heading.text}</a> - ))} - </nav> - </aside> - - <article class="article-shell release-shell"> - <header class="article-header release-header"> - <p class="post-meta"> - <a href="/releases/">Releases</a> - <span>{release.data.version}</span> - <time datetime={release.data.date.toISOString()}>{release.data.date.toLocaleDateString('en', { - year: 'numeric', - month: 'long', - day: 'numeric', - })}</time> - </p> - <h1>{release.data.title}</h1> - <p>{release.data.description}</p> - </header> - <div class="article-prose release-prose"> - <Content /> - </div> - </article> - </div> - </main> - - <script> - const headings = Array.from(document.querySelectorAll('.article-prose h2[id]')); - const links = new Map( - Array.from(document.querySelectorAll('[data-heading-link]')).map((link) => [ - link.getAttribute('data-heading-link'), - link, - ]), - ); - - function setActive(id) { - for (const link of links.values()) link.removeAttribute('aria-current'); - links.get(id)?.setAttribute('aria-current', 'true'); - } - - if (headings.length > 0 && links.size > 0) { - setActive(headings[0].id); - const observer = new IntersectionObserver( - (entries) => { - const visible = entries - .filter((entry) => entry.isIntersecting) - .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]; - if (visible?.target instanceof HTMLElement) setActive(visible.target.id); - }, - { rootMargin: '-18% 0px -68% 0px', threshold: 0 }, - ); - for (const heading of headings) observer.observe(heading); - } - </script> -</MarketingLayout> diff --git a/website/src/pages/releases/index.astro b/website/src/pages/releases/index.astro deleted file mode 100644 index 48263ab9..00000000 --- a/website/src/pages/releases/index.astro +++ /dev/null @@ -1,42 +0,0 @@ ---- -import { getCollection } from 'astro:content'; -import MarketingLayout from '../../components/MarketingLayout.astro'; - -const releases = (await getCollection('releases')) - .filter((release) => !release.data.draft) - .map((release) => ({ ...release, slug: release.data.path })) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); ---- - -<MarketingLayout - title="Herdr releases" - description="Product-focused release notes for Herdr." - canonicalPath="/releases/" -> - <main class="content-shell"> - <section class="page-hero page-hero-compact"> - <p class="eyebrow"><span class="status-dot dot-working"></span>Releases</p> - <h1>What changed in Herdr.</h1> - <p class="hero-lede"> - See what the herd has been working on. - </p> - </section> - - <section class="section section-narrow" aria-label="Releases"> - <div class="post-list"> - {releases.map((release) => ( - <article class="post-card"> - <a class="post-card-link" href={`/releases/${release.slug}/`}> - <div> - <p class="release-card-version">{release.data.version}</p> - <h2>{release.data.title}</h2> - <p>{release.data.description}</p> - </div> - <span aria-hidden="true">-></span> - </a> - </article> - ))} - </div> - </section> - </main> -</MarketingLayout> |
