22import Breadcrumbs from " ./Breadcrumbs.astro"
33import InstallGuide from " ./InstallGuide.astro"
44import PluginHub from " ./PluginHub.astro"
5- import Icon , { type IconName } from " ./Icon.astro"
5+ import PluginPage from " ./PluginPage.astro"
6+ import Icon from " ./Icon.astro"
67import SkillDownload from " ./SkillDownload.astro"
78import Layout from " ../layouts/Layout.astro"
89import {
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 {
2425const { page } = Astro .props
2526const canonical = ` ${site .url }/${page .path }/ `
2627const 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 ))
2829const settingsCards = [
2930 {
3031 title: " Configure Claude Code" ,
@@ -222,23 +223,18 @@ const structuredData = { "@context": "https://schema.org", "@graph": graph }
222223const 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 >
0 commit comments