Skip to content

Commit 216ec22

Browse files
authored
feat: redesign home and plugin detail pages (#328)
Merge the tool row and install band on the home page, and give plugin pages a two-column hero, a single contents section, and related plugins.
1 parent 7a54dba commit 216ec22

10 files changed

Lines changed: 411 additions & 369 deletions

File tree

‎site/src/components/ContentPage.astro‎

Lines changed: 11 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
import Breadcrumbs from "./Breadcrumbs.astro"
33
import InstallGuide from "./InstallGuide.astro"
44
import PluginHub from "./PluginHub.astro"
5-
import Icon, { type IconName } from "./Icon.astro"
5+
import PluginPage from "./PluginPage.astro"
6+
import Icon from "./Icon.astro"
67
import SkillDownload from "./SkillDownload.astro"
78
import Layout from "../layouts/Layout.astro"
89
import {
9-
codingTools,
1010
configurationDocuments,
1111
pluginByName,
1212
plugins,
1313
repositoryUrl,
1414
site,
1515
skills,
16+
skillsByPlugin,
1617
sourceDocuments,
1718
type ContentPage,
1819
} from "../lib/content"
@@ -24,7 +25,7 @@ interface Props {
2425
const { page } = Astro.props
2526
const canonical = `${site.url}/${page.path}/`
2627
const homeCrumb = { label: "Home", href: "/" }
27-
const skillGroups = Object.entries(Object.groupBy(skills, (skill) => skill.plugin)).sort(([a], [b]) => a.localeCompare(b))
28+
const skillGroups = Object.entries(skillsByPlugin).sort(([a], [b]) => a.localeCompare(b))
2829
const settingsCards = [
2930
{
3031
title: "Configure Claude Code",
@@ -222,23 +223,18 @@ const structuredData = { "@context": "https://schema.org", "@graph": graph }
222223
const providerFiles = page.kind === "provider"
223224
? configurationDocuments.filter((file) => file.provider === page.provider)
224225
: []
225-
const pluginSkills = plugin ? skills.filter((item) => item.plugin === plugin.name) : []
226-
const codingToolIcons = new Map(codingTools.map(({ id, icon }) => [id, icon]))
227-
const installDemoCommands = plugin?.installCommands.map((item) => ({
228-
...item,
229-
icon: codingToolIcons.get(item.id) as IconName,
230-
})) || []
231-
const initialInstallDemo = installDemoCommands[0]
232226
---
233227

234228
<Layout locale="en" variant="settings" {title} {description} {canonical} alternates={{ en: canonical }} {structuredData}>
235229
<article class="content-page" data-content-type={page.kind === "installGuide" ? "How-to" : page.kind.includes("Hub") ? "Landing" : "Reference"}>
236230
<Breadcrumbs items={breadcrumbs} />
237-
<header class="content-hero">
238-
<h1>{title}</h1>
239-
<p>{intro}</p>
240-
{page.kind === "skillHub" && <a class="button primary" href="/guides/install-agent-skills/">Read the install guide <Icon name="arrow" size={18} /></a>}
241-
</header>
231+
{plugin ? <PluginPage {plugin} /> : (
232+
<header class="content-hero">
233+
<h1>{title}</h1>
234+
<p>{intro}</p>
235+
{page.kind === "skillHub" && <a class="button primary" href="/guides/install-agent-skills/">Read the install guide <Icon name="arrow" size={18} /></a>}
236+
</header>
237+
)}
242238

243239
{page.kind === "pluginHub" && <PluginHub />}
244240

@@ -301,94 +297,6 @@ const initialInstallDemo = installDemoCommands[0]
301297
</div>
302298
)}
303299

304-
{plugin && (
305-
<div class="content-sections">
306-
<section class="content-section install-terminal-section" data-install-terminal>
307-
<h2>Install the plugin in your coding agent</h2>
308-
<p><a class="inline-link" href="/guides/install-agent-skills/#claude-code">Add the marketplace once</a>, then run the command for your tool.</p>
309-
<div class="install-tool-tabs" role="tablist" aria-label="Choose a coding agent">
310-
{installDemoCommands.map(({ tool, command, icon, output }, index) => (
311-
<button
312-
id={`install-tab-${plugin.name}-${index}`}
313-
class:list={["install-tool-tab", { "is-active": index === 0 }]}
314-
type="button"
315-
role="tab"
316-
aria-controls={`install-terminal-${plugin.name}`}
317-
aria-selected={index === 0 ? "true" : "false"}
318-
tabindex={index === 0 ? 0 : -1}
319-
data-install-tab
320-
data-command={command}
321-
data-output-primary={output[0]}
322-
data-output-secondary={output[1]}
323-
>
324-
<Icon name={icon} size={22} />
325-
<span>{tool}</span>
326-
</button>
327-
))}
328-
</div>
329-
<div
330-
id={`install-terminal-${plugin.name}`}
331-
class="install-terminal"
332-
role="tabpanel"
333-
aria-labelledby={`install-tab-${plugin.name}-0`}
334-
data-install-panel
335-
>
336-
<div class="install-terminal-bar">
337-
<span class="terminal-lights" aria-hidden="true"><i></i><i></i><i></i></span>
338-
<span class="install-terminal-title">Terminal · <strong data-install-title>{initialInstallDemo.tool}</strong></span>
339-
<button class="terminal-copy" type="button" data-copy={initialInstallDemo.command} aria-label={`Copy ${initialInstallDemo.tool} install command`}>
340-
<Icon name="copy" size={18} />
341-
<span data-install-copy-label>Copy command</span>
342-
</button>
343-
</div>
344-
<div class="install-terminal-body">
345-
<div class="install-command-line">
346-
<span class="install-prompt" aria-hidden="true">$</span>
347-
<code data-install-command>{initialInstallDemo.command}</code>
348-
<span class="typing-cursor" aria-hidden="true"></span>
349-
</div>
350-
<div class="install-output" aria-hidden="true">
351-
<p data-install-output-primary>{initialInstallDemo.output[0]}</p>
352-
<p class="install-success" data-install-output-secondary>{initialInstallDemo.output[1]}</p>
353-
</div>
354-
<span class="sr-only" aria-live="polite" data-install-announcement></span>
355-
</div>
356-
<div class="install-terminal-progress">
357-
<span class="install-progress-track" aria-hidden="true"><i data-install-progress></i></span>
358-
<span data-install-count>1 of {installDemoCommands.length}</span>
359-
<button type="button" data-install-toggle aria-label="Pause installation demo" title="Pause installation demo">
360-
<span class="pause-glyph"><Icon name="pause" size={16} /></span>
361-
<span class="play-glyph"><Icon name="play" size={16} /></span>
362-
</button>
363-
</div>
364-
</div>
365-
</section>
366-
<section class="content-section">
367-
<h2>See what the plugin includes</h2>
368-
<p>These components come from the current plugin directory, not a hand-written feature list.</p>
369-
<dl class="component-list">
370-
<div><dt>Supported tools</dt><dd>{plugin.tools.join(", ")}</dd></div>
371-
<div><dt>Skills</dt><dd>{plugin.components?.skills.join(", ") || "None"}</dd></div>
372-
<div><dt>Agents</dt><dd>{plugin.components?.agents.join(", ") || "None"}</dd></div>
373-
<div><dt>Commands</dt><dd>{plugin.components?.commands.join(", ") || "None"}</dd></div>
374-
<div><dt>Hooks</dt><dd>{plugin.components?.hooks ? "Included" : "None"}</dd></div>
375-
</dl>
376-
</section>
377-
{pluginSkills.length > 0 && (
378-
<section class="content-section">
379-
<h2>Download this plugin’s skills</h2>
380-
<p>Each release ZIP works in ChatGPT and Claude.ai. Review the source before uploading it.</p>
381-
<div class="skill-stack">{pluginSkills.map((item) => <SkillDownload skill={item} heading="h3" />)}</div>
382-
</section>
383-
)}
384-
<section class="content-section source-facts">
385-
<h2>Verify the source</h2>
386-
<p>Version {plugin.version}. License: {plugin.license || "See source"}.</p>
387-
<a class="text-link" href={plugin.href}>Open the plugin files on GitHub <Icon name="arrow" size={17} /></a>
388-
</section>
389-
</div>
390-
)}
391-
392300
{skill && (
393301
<div class="content-sections">
394302
<section class="content-section"><SkillDownload {skill} /></section>
@@ -419,117 +327,3 @@ const initialInstallDemo = installDemoCommands[0]
419327
{page.kind === "installGuide" && <InstallGuide />}
420328
</article>
421329
</Layout>
422-
423-
<script>
424-
document.querySelectorAll<HTMLElement>("[data-install-terminal]").forEach((demo) => {
425-
const tabs = [...demo.querySelectorAll<HTMLButtonElement>("[data-install-tab]")]
426-
const panel = demo.querySelector<HTMLElement>("[data-install-panel]")!
427-
const title = demo.querySelector<HTMLElement>("[data-install-title]")!
428-
const command = demo.querySelector<HTMLElement>("[data-install-command]")!
429-
const primaryOutput = demo.querySelector<HTMLElement>("[data-install-output-primary]")!
430-
const secondaryOutput = demo.querySelector<HTMLElement>("[data-install-output-secondary]")!
431-
const announcement = demo.querySelector<HTMLElement>("[data-install-announcement]")!
432-
const progress = demo.querySelector<HTMLElement>("[data-install-progress]")!
433-
const count = demo.querySelector<HTMLElement>("[data-install-count]")!
434-
const copyButton = demo.querySelector<HTMLButtonElement>("[data-copy]")!
435-
const toggle = demo.querySelector<HTMLButtonElement>("[data-install-toggle]")!
436-
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
437-
let current = 0
438-
let paused = false
439-
let visible = false
440-
const timers = new Set<number>()
441-
442-
const clearTimers = () => {
443-
timers.forEach(window.clearTimeout)
444-
timers.clear()
445-
}
446-
const later = (callback: () => void, delay: number) => {
447-
const timer = window.setTimeout(() => {
448-
timers.delete(timer)
449-
callback()
450-
}, delay)
451-
timers.add(timer)
452-
}
453-
const showComplete = (tab: HTMLButtonElement) => {
454-
demo.dataset.phase = "complete"
455-
announcement.textContent = tab.dataset.outputSecondary || "Installation complete"
456-
}
457-
const activate = (index: number) => {
458-
clearTimers()
459-
current = (index + tabs.length) % tabs.length
460-
const tab = tabs[current]
461-
const fullCommand = tab.dataset.command || ""
462-
tabs.forEach((item, tabIndex) => {
463-
const active = tabIndex === current
464-
item.classList.toggle("is-active", active)
465-
item.setAttribute("aria-selected", String(active))
466-
item.tabIndex = active ? 0 : -1
467-
})
468-
panel.setAttribute("aria-labelledby", tab.id)
469-
title.textContent = tab.textContent?.trim() || "Terminal"
470-
copyButton.dataset.copy = fullCommand
471-
copyButton.setAttribute("aria-label", `Copy ${title.textContent} install command`)
472-
copyButton.classList.remove("is-copied")
473-
count.textContent = `${current + 1} of ${tabs.length}`
474-
progress.classList.remove("is-counting")
475-
demo.dataset.phase = "typing"
476-
announcement.textContent = ""
477-
command.textContent = fullCommand
478-
primaryOutput.textContent = tab.dataset.outputPrimary || ""
479-
secondaryOutput.textContent = tab.dataset.outputSecondary || ""
480-
481-
if (reducedMotion.matches || paused) {
482-
showComplete(tab)
483-
return
484-
}
485-
486-
command.textContent = ""
487-
let character = 0
488-
const typeNext = () => {
489-
command.textContent = fullCommand.slice(0, ++character)
490-
if (character < fullCommand.length) later(typeNext, 24)
491-
else later(() => {
492-
demo.dataset.phase = "output"
493-
later(() => {
494-
demo.dataset.phase = "complete"
495-
announcement.textContent = tab.dataset.outputSecondary || "Installation complete"
496-
progress.classList.add("is-counting")
497-
if (tabs.length > 1) later(() => activate(current + 1), 2600)
498-
}, 620)
499-
}, 320)
500-
}
501-
later(typeNext, 260)
502-
}
503-
const resume = () => {
504-
clearTimers()
505-
if (visible && !document.hidden && !paused) activate(current)
506-
}
507-
508-
tabs.forEach((tab, index) => {
509-
tab.addEventListener("click", () => activate(index))
510-
tab.addEventListener("keydown", (event) => {
511-
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
512-
event.preventDefault()
513-
const next = index + (event.key === "ArrowRight" ? 1 : -1)
514-
tabs[(next + tabs.length) % tabs.length].focus()
515-
activate(next)
516-
})
517-
})
518-
toggle.addEventListener("click", () => {
519-
paused = !paused
520-
demo.toggleAttribute("data-paused", paused)
521-
toggle.setAttribute("aria-label", `${paused ? "Play" : "Pause"} installation demo`)
522-
toggle.title = `${paused ? "Play" : "Pause"} installation demo`
523-
if (paused) {
524-
clearTimers()
525-
showComplete(tabs[current])
526-
} else activate(current)
527-
})
528-
new IntersectionObserver(([entry]) => {
529-
visible = entry.isIntersecting
530-
resume()
531-
}).observe(demo)
532-
document.addEventListener("visibilitychange", resume)
533-
reducedMotion.addEventListener("change", resume)
534-
})
535-
</script>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
import ToolIcons from "./ToolIcons.astro"
3+
import type { plugins } from "../lib/content"
4+
5+
interface Props {
6+
plugin: (typeof plugins)[number]
7+
}
8+
9+
const { plugin } = Astro.props
10+
---
11+
12+
<a class="plugin-card" href={`/plugins/${plugin.name}/`}>
13+
<strong style={`view-transition-name: plugin-name-${plugin.name}`}>{plugin.name}</strong>
14+
<span class="plugin-card-description">{plugin.description}</span>
15+
<span class="plugin-card-meta">
16+
<ToolIcons {plugin} />
17+
<span>{plugin.summary}</span>
18+
</span>
19+
</a>

‎site/src/components/PluginHub.astro‎

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
---
22
import Icon from "./Icon.astro"
3-
import { codingTools, plugins } from "../lib/content"
3+
import PluginCard from "./PluginCard.astro"
4+
import { plugins } from "../lib/content"
45
56
const pluginCategories = Object.entries(Object.groupBy(plugins, (plugin) => plugin.category || "other")).sort(([a], [b]) =>
67
a.localeCompare(b),
78
)
8-
const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? "" : "s"}`
9-
const componentSummary = (item: (typeof plugins)[number]) =>
10-
[
11-
item.components?.skills.length && plural(item.components.skills.length, "skill"),
12-
item.components?.agents.length && plural(item.components.agents.length, "agent"),
13-
item.components?.commands.length && plural(item.components.commands.length, "command"),
14-
item.components?.hooks && "hooks",
15-
].filter(Boolean).join(", ")
169
---
1710

1811
<div class="hub" data-plugin-hub>
@@ -29,28 +22,14 @@ const componentSummary = (item: (typeof plugins)[number]) =>
2922
))}
3023
</div>
3124
</div>
32-
<ul class="hub-grid" role="list">
25+
<ul class="plugin-grid" role="list">
3326
{plugins.map((item) => (
3427
<li
3528
style={`view-transition-name: plugin-${item.name}`}
3629
data-category={item.category || "other"}
3730
data-search={item.search}
3831
>
39-
<a href={`/plugins/${item.name}/`}>
40-
<strong>{item.name}</strong>
41-
<span class="hub-description">{item.description}</span>
42-
<span class="hub-meta">
43-
<span class="hub-tools">
44-
{codingTools.map((tool) => (
45-
<span class:list={{ "is-off": !item[`${tool.id}Command`] }} title={`${tool.label}${item[`${tool.id}Command`] ? "" : " not supported"}`}>
46-
<Icon name={tool.icon} size={15} />
47-
</span>
48-
))}
49-
<span class="sr-only">Works with {item.tools.join(", ")}</span>
50-
</span>
51-
<span>{componentSummary(item)}</span>
52-
</span>
53-
</a>
32+
<PluginCard plugin={item} />
5433
</li>
5534
))}
5635
</ul>

0 commit comments

Comments
 (0)