mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-01 23:12:29 +00:00
Merge branch 'main' into all-contributors/add-thesurenk
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
---
|
||||
// Custom override of Starlight's LanguageSelect.
|
||||
//
|
||||
// By default Starlight renders the language dropdown on every page as soon as
|
||||
// more than one locale is configured. Most of this site's docs are English-only,
|
||||
// so we only want the picker to appear on pages that actually have a translation
|
||||
// in a non-English locale. This override checks the docs collection for a
|
||||
// translated version of the current page and renders the stock picker only when
|
||||
// one exists; otherwise it renders nothing (no dropdown).
|
||||
import Default from "@astrojs/starlight/components/LanguageSelect.astro";
|
||||
import config from "virtual:starlight/user-config";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
// Locale directory names configured for the site, excluding the English root.
|
||||
const localeCodes = Object.keys(config.locales ?? {}).filter(
|
||||
(code) => code !== "root"
|
||||
);
|
||||
|
||||
/** Strip a leading `<locale>/` segment from a docs entry id, if present. */
|
||||
function baseSlug(id) {
|
||||
const [first, ...rest] = id.split("/");
|
||||
return localeCodes.includes(first) ? rest.join("/") : id;
|
||||
}
|
||||
|
||||
// Set of base slugs (locale-stripped) that have at least one non-English
|
||||
// translation available in the docs collection.
|
||||
const docs = await getCollection("docs");
|
||||
const translatedBaseSlugs = new Set();
|
||||
for (const entry of docs) {
|
||||
const [first, ...rest] = entry.id.split("/");
|
||||
if (localeCodes.includes(first)) {
|
||||
translatedBaseSlugs.add(rest.join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
const currentBaseSlug = baseSlug(Astro.locals.starlightRoute.id);
|
||||
const hasTranslations = translatedBaseSlugs.has(currentBaseSlug);
|
||||
---
|
||||
|
||||
{hasTranslations && <Default {...Astro.props} />}
|
||||
@@ -1,242 +0,0 @@
|
||||
---
|
||||
// Modal component for viewing file contents
|
||||
---
|
||||
|
||||
<div
|
||||
id="file-modal"
|
||||
class="modal hidden"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header-top">
|
||||
<h3 id="modal-title">File</h3>
|
||||
<div class="modal-actions">
|
||||
<div id="modal-file-switcher" class="modal-file-switcher hidden">
|
||||
<span class="modal-file-switcher-label">File</span>
|
||||
<div id="modal-file-dropdown" class="modal-file-dropdown">
|
||||
<button
|
||||
type="button"
|
||||
id="modal-file-button"
|
||||
class="btn btn-secondary modal-file-button"
|
||||
aria-label="Select file to preview"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span id="modal-file-button-label">SKILL.md</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="modal-file-toggle"
|
||||
class="btn btn-secondary modal-file-toggle"
|
||||
aria-label="Open file menu"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="modal-file-menu" class="modal-file-menu" role="menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="install-command-btn"
|
||||
class="btn btn-secondary hidden"
|
||||
aria-label="Copy install command"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
|
||||
></path>
|
||||
<path
|
||||
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Copy Install</span>
|
||||
</button>
|
||||
<button
|
||||
id="raw-btn"
|
||||
class="btn btn-secondary hidden"
|
||||
aria-label="View raw file"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M2.3 3.2a.75.75 0 0 1 1.06 0L8 7.94l4.64-4.72a.75.75 0 1 1 1.06 1.06L9.06 8l4.64 4.72a.75.75 0 1 1-1.06 1.06L8 8.06l-4.64 4.72a.75.75 0 1 1-1.06-1.06L6.94 8 2.3 3.28a.75.75 0 0 1 0-1.06z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Raw</span>
|
||||
</button>
|
||||
<button
|
||||
id="render-btn"
|
||||
class="btn btn-secondary"
|
||||
aria-label="Render file"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M2 2h12v1H2V2zm0 3h12v1H2V5zm0 3h12v1H2V8zm0 3h12v1H2v-1z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Render</span>
|
||||
</button>
|
||||
<button
|
||||
id="copy-btn"
|
||||
class="btn btn-secondary"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
|
||||
></path>
|
||||
<path
|
||||
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Copy</span>
|
||||
</button>
|
||||
<button
|
||||
id="download-btn"
|
||||
class="btn btn-secondary"
|
||||
aria-label="Download file"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7.47 10.78a.75.75 0 0 0 1.06 0l3.75-3.75a.75.75 0 0 0-1.06-1.06L8.75 8.44V1.75a.75.75 0 0 0-1.5 0v6.69L4.78 5.97a.75.75 0 0 0-1.06 1.06l3.75 3.75ZM3.75 13a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Download</span>
|
||||
</button>
|
||||
<button
|
||||
id="share-btn"
|
||||
class="btn btn-secondary"
|
||||
aria-label="Copy link to clipboard"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-.025 5.45a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 1 1-2.83-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25z"
|
||||
></path>
|
||||
</svg>
|
||||
<span aria-hidden="true">Share</span>
|
||||
</button>
|
||||
<div
|
||||
id="install-dropdown"
|
||||
class="install-dropdown"
|
||||
style="display: none;"
|
||||
>
|
||||
<a
|
||||
id="install-btn-main"
|
||||
class="btn btn-primary install-btn-main"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Install
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary install-btn-toggle"
|
||||
aria-label="Install options"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="install-dropdown-menu" role="menu">
|
||||
<a
|
||||
id="install-vscode"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
role="menuitem"
|
||||
>
|
||||
VS Code
|
||||
</a>
|
||||
<a
|
||||
id="install-insiders"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
role="menuitem"
|
||||
>
|
||||
VS Code Insiders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="close-modal"
|
||||
class="btn btn-icon"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="modal-content"><code /></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,9 +21,10 @@ const KIND_LABELS: Record<string, string> = {
|
||||
hook: "Hooks",
|
||||
prompt: "Commands",
|
||||
extension: "Extensions",
|
||||
mcp: "MCP Servers",
|
||||
};
|
||||
|
||||
const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension"];
|
||||
const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension", "mcp"];
|
||||
|
||||
function githubHref(itemPath: string): string {
|
||||
const normalized = itemPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
||||
|
||||
@@ -77,7 +77,7 @@ Agentic Workflows are ideal when you need **autonomous, event-driven automation*
|
||||
|
||||
## Using Workflows from Awesome Copilot
|
||||
|
||||
The [Awesome Copilot workflows page](../../workflows/) hosts a growing collection of community-contributed workflows. Here's how to install and use them.
|
||||
The [repository workflows directory](https://github.com/github/awesome-copilot/tree/main/workflows) hosts a growing collection of community-contributed workflows. Here's how to install and use them.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -89,7 +89,7 @@ gh extension install github/gh-aw
|
||||
|
||||
### Installing a Workflow
|
||||
|
||||
1. **Browse** the [workflows collection](../../workflows/) and find one that fits your needs
|
||||
1. **Browse** the [workflows collection](https://github.com/github/awesome-copilot/tree/main/workflows) and find one that fits your needs
|
||||
2. **Copy** the workflow `.md` file into your repository's `.github/workflows/` directory
|
||||
3. **Compile** the workflow to generate the Actions lock file:
|
||||
|
||||
@@ -211,8 +211,6 @@ gh aw compile --validate --no-emit workflows/my-new-workflow.md
|
||||
## Learn More
|
||||
|
||||
- **Official documentation**: [GitHub Agentic Workflows](https://gh.io/gh-aw) — full specification and reference
|
||||
- **Browse workflows**: [Awesome Copilot Workflows](../../workflows/) — community-contributed collection
|
||||
- **Contributing guide**: [CONTRIBUTING.md](https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md#adding-agentic-workflows) — detailed contribution guidelines
|
||||
- **Related**: [Automating with Hooks](../automating-with-hooks/) — deterministic automation for Copilot agent sessions
|
||||
- **Related**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — the agent that powers agentic workflows
|
||||
|
||||
|
||||
@@ -681,7 +681,6 @@ A: Yes. Hooks are especially valuable with the coding agent because they provide
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Explore Examples**: Browse the [Hooks Directory](../../hooks/) for ready-to-use hook configurations
|
||||
- **Build Agents**: [Building Custom Agents](../building-custom-agents/) — Create agents that complement hooks
|
||||
- **Automate Further**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — Run hooks in autonomous agent sessions
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: 'Using Automations in the GitHub Copilot app'
|
||||
description: 'A practical guide to getting started with Copilot app automations using templates, iterative refinement, and real-world examples.'
|
||||
title: "Using Automations in the GitHub Copilot app"
|
||||
description: "A practical guide to getting started with Copilot app automations using templates, iterative refinement, and real-world examples."
|
||||
authors:
|
||||
- GitHub Copilot Learning Hub Team
|
||||
lastUpdated: 2026-06-17
|
||||
estimatedReadingTime: '10 minutes'
|
||||
estimatedReadingTime: "10 minutes"
|
||||
tags:
|
||||
- copilot-app
|
||||
- automations
|
||||
@@ -61,13 +61,15 @@ If your first version is only 70% right, that is normal. The fastest path is to
|
||||
|
||||
Here is a real in-app automation used on the `github/awesome-copilot` repository:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | Awesome Copilot daily PR summary |
|
||||
| **Interval** | Daily at 09:00 |
|
||||
| **Mode** | Autopilot |
|
||||
| Field | Value |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Awesome Copilot daily PR summary |
|
||||
| **Interval** | Daily at 09:00 |
|
||||
| **Mode** | Autopilot |
|
||||
| **What it does** | Pulls open PRs via `gh api`, filters to updates in the last 24 hours, and returns a concise summary table |
|
||||
|
||||
[](ghapp://automations/new?name=Awesome%20Copilot%20daily%20PR%20summary&trigger=daily&time=09%3A00&prompt=Pulls%20open%20PRs%20via%20gh%20api%2C%20filters%20to%20updates%20in%20the%20last%2024%20hours%2C%20and%20returns%20a%20concise%20summary%20table)
|
||||
|
||||
Why this works well:
|
||||
|
||||
- It is narrowly scoped (one repo, one reporting goal).
|
||||
|
||||
@@ -327,7 +327,7 @@ This repository provides a curated collection of agents, skills, and hooks desig
|
||||
|
||||
### Adding Hooks from This Repo
|
||||
|
||||
1. Browse the [Hooks Directory](../../hooks/) for automation hooks
|
||||
1. Browse the [Hooks documentation](https://github.com/github/awesome-copilot/blob/main/docs/README.hooks.md) for automation hooks
|
||||
2. Copy the `hooks.json` content into a file in `.github/hooks/` in your repository
|
||||
3. Copy any referenced scripts alongside it
|
||||
4. The hooks will run automatically during coding agent sessions
|
||||
@@ -451,6 +451,6 @@ A: Yes. You can specify which agent to use when assigning work — the coding ag
|
||||
- **Add Guardrails**: [Automating with Hooks](../automating-with-hooks/) — Ensure code quality in autonomous sessions
|
||||
- **Build Custom Agents**: [Building Custom Agents](../building-custom-agents/) — Create specialized agents for the coding agent to use
|
||||
- **Explore Configuration**: [Copilot Configuration Basics](../copilot-configuration-basics/) — Set up repository-level customizations
|
||||
- **Browse Community Resources**: Explore the [Agents](../../agents/), [Skills](../../skills/), and [Hooks](../../hooks/) directories for ready-to-use resources
|
||||
- **Browse Community Resources**: Explore the [Agents](../../agents/), [Skills](../../skills/), and [Plugins](../../plugins/) directories for ready-to-use resources
|
||||
|
||||
---
|
||||
|
||||
@@ -42,9 +42,7 @@ const TYPE_PAGES: Record<string, string> = {
|
||||
};
|
||||
|
||||
// Resource types that have a dedicated detail page at /<type>/<id>/. Search
|
||||
// results for these should deep-link to the canonical detail page rather than
|
||||
// the listing page with an inert #file= hash (listing pages no longer open a
|
||||
// modal from that hash).
|
||||
// results for these should deep-link to the canonical detail page.
|
||||
const DETAIL_ROUTE_TYPES = new Set([
|
||||
"agent",
|
||||
"instruction",
|
||||
@@ -118,9 +116,7 @@ export default function pagefindResources(): AstroIntegration {
|
||||
|
||||
const url = hasDetailPage
|
||||
? `${base}${record.type}/${encodeURIComponent(record.id)}/`
|
||||
: `${base}${typePage.slice(1)}#file=${encodeURIComponent(
|
||||
record.path
|
||||
)}`;
|
||||
: `${base}${typePage.slice(1)}`;
|
||||
const typeLabel = TYPE_LABELS[record.type] || record.type;
|
||||
|
||||
const addResult = await index.addCustomRecord({
|
||||
|
||||
@@ -504,7 +504,6 @@ import PageHeader from '../components/PageHeader.astro';
|
||||
<td align="center" size="13px" colspan="7">
|
||||
<img src="https://raw.githubusercontent.com/all-contributors/all-contributors-cli/1b8533af435da9854653492b1327a23a4dbd0a10/assets/logo-small.svg">
|
||||
<a href="https://all-contributors.js.org/docs/en/bot/usage">Add your contributions</a>
|
||||
</img>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
@@ -46,6 +46,7 @@ interface ExtensionEntry extends HeaderItem {
|
||||
installUrl?: string | null;
|
||||
installCommand?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
externalSource?: string | null;
|
||||
external?: boolean;
|
||||
author?: ExtensionAuthor | null;
|
||||
keywords?: string[];
|
||||
@@ -117,6 +118,7 @@ const installUrl = safeUrl(
|
||||
: "")
|
||||
);
|
||||
const sourceUrl = safeUrl(item.sourceUrl);
|
||||
const externalSource = (item.externalSource || "").trim();
|
||||
const installCommand =
|
||||
item.installCommand ||
|
||||
(item.pluginName
|
||||
@@ -124,16 +126,13 @@ const installCommand =
|
||||
: "");
|
||||
|
||||
const githubUrl = isExternal
|
||||
? sourceUrl || installUrl || GITHUB_TREE
|
||||
? sourceUrl || GITHUB_TREE
|
||||
: item.path
|
||||
? `${GITHUB_TREE}/${item.path}`
|
||||
: installUrl || GITHUB_TREE;
|
||||
|
||||
// Sidebar "Source" shows item.path; external extensions have no repo path.
|
||||
const sidebarItem =
|
||||
isExternal && (sourceUrl || installUrl)
|
||||
? { ...item, path: sourceUrl || installUrl }
|
||||
: item;
|
||||
const sidebarItem = isExternal && sourceUrl ? { ...item, path: sourceUrl } : item;
|
||||
|
||||
const shortHash = (value: string) =>
|
||||
/^[0-9a-f]{40}$/i.test(value) ? value.slice(0, 7) : value;
|
||||
@@ -262,22 +261,13 @@ if (!isExternal && item.ref) {
|
||||
>
|
||||
{
|
||||
isExternal ? (
|
||||
<div class="skill-install" slot="install">
|
||||
<p class="skill-install-label">Install this extension</p>
|
||||
<p class="skill-install-note">
|
||||
This extension is maintained in an external repository.
|
||||
</p>
|
||||
{installUrl && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary skill-install-url-btn"
|
||||
data-action="copy-install-url"
|
||||
data-install-url={installUrl}
|
||||
>
|
||||
Copy install URL
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<PluginInstall
|
||||
slot="install"
|
||||
isExternal={true}
|
||||
externalSource={externalSource || null}
|
||||
label="Install this extension"
|
||||
note="This extension is maintained in an external repository."
|
||||
/>
|
||||
) : (
|
||||
<PluginInstall
|
||||
slot="install"
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
---
|
||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||
import BackToTop from "../../components/BackToTop.astro";
|
||||
import hooksData from "../../../public/data/hooks.json";
|
||||
import RawMarkdown from "../../components/pages/RawMarkdown.astro";
|
||||
import type { HeaderItem } from "../../components/pages/Header.astro";
|
||||
import DetailHeader from "../../components/pages/Header.astro";
|
||||
import Breadcrumb from "../../components/pages/Breadcrumb.astro";
|
||||
import Sidebar from "../../components/pages/Sidebar.astro";
|
||||
import FileBrowser from "../../components/pages/FileBrowser.astro";
|
||||
import { readResourceMarkdown, formatLastUpdated } from "../../lib/detail-page";
|
||||
|
||||
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
|
||||
const GITHUB_TREE_BASE = "https://github.com/github/awesome-copilot/tree/main";
|
||||
|
||||
interface HookFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface HookItem extends HeaderItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
readmeFile: string;
|
||||
readmeFileName?: string;
|
||||
files: HookFile[];
|
||||
hooks: string[];
|
||||
tags: string[];
|
||||
assets: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
return (hooksData.items as HookItem[]).map((item) => ({
|
||||
params: { id: item.id },
|
||||
props: { item },
|
||||
}));
|
||||
}
|
||||
|
||||
const { item } = Astro.props as { item: HookItem };
|
||||
|
||||
const { markdownHtml, frontmatterText, rawMarkdown } = readResourceMarkdown(
|
||||
item.readmeFile
|
||||
);
|
||||
const lastUpdated = formatLastUpdated(item.lastUpdated);
|
||||
const githubUrl = `${GITHUB_TREE_BASE}/${item.path}`;
|
||||
const readmeFileName = item.readmeFileName ?? "README.md";
|
||||
|
||||
const fileCount = item.files.length;
|
||||
const chips: {
|
||||
title: string;
|
||||
items: string[];
|
||||
filterBase?: string;
|
||||
filterParam?: string;
|
||||
}[] = [];
|
||||
if (item.hooks.length > 0) {
|
||||
chips.push({ title: "Hook events", items: item.hooks });
|
||||
}
|
||||
chips.push({
|
||||
title: "Files",
|
||||
items: [`${fileCount} file${fileCount === 1 ? "" : "s"}`],
|
||||
});
|
||||
if (item.tags.length > 0) {
|
||||
chips.push({
|
||||
title: "Tags",
|
||||
items: item.tags,
|
||||
filterBase: "/hooks/",
|
||||
filterParam: "tag",
|
||||
});
|
||||
}
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
frontmatter={{
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
template: "splash",
|
||||
prev: false,
|
||||
next: false,
|
||||
editUrl: false,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="main-content"
|
||||
class="resource-detail-page hook-detail-page"
|
||||
data-file-browser-page
|
||||
data-bundle-id={item.id}
|
||||
data-path={item.readmeFile}
|
||||
>
|
||||
<RawMarkdown markdown={rawMarkdown} />
|
||||
<div class="container">
|
||||
<Breadcrumb
|
||||
paths={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Hooks", href: "/hooks/" },
|
||||
]}
|
||||
title={item.title}
|
||||
/>
|
||||
|
||||
<DetailHeader item={item} />
|
||||
|
||||
<div class="detail-layout">
|
||||
<div class="detail-main">
|
||||
<FileBrowser
|
||||
files={item.files}
|
||||
primaryPath={item.readmeFile}
|
||||
primaryName={readmeFileName}
|
||||
primaryHtml={markdownHtml}
|
||||
githubBase={GITHUB_BASE}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sidebar
|
||||
item={item}
|
||||
lastUpdated={lastUpdated ?? undefined}
|
||||
frontmatterText={frontmatterText || undefined}
|
||||
githubUrl={githubUrl}
|
||||
chips={chips}
|
||||
>
|
||||
<div class="skill-install" slot="install">
|
||||
<p class="skill-install-label">Install this hook</p>
|
||||
<p class="skill-install-note">
|
||||
Hooks are multi-file bundles with no CLI installer. Download the
|
||||
ZIP and copy the files into your project's hooks directory.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary skill-download-btn"
|
||||
data-action="download-zip"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
><path
|
||||
d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"
|
||||
></path><path
|
||||
d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"
|
||||
></path></svg
|
||||
>
|
||||
Download ZIP
|
||||
</button>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
|
||||
<script>
|
||||
import "../../scripts/pages/file-browser";
|
||||
</script>
|
||||
</StarlightPage>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import ContributeCTA from '../components/ContributeCTA.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
|
||||
import BackToTop from '../components/BackToTop.astro';
|
||||
import hooksData from '../../public/data/hooks.json';
|
||||
import { renderHooksHtml, sortHooks } from '../scripts/pages/hooks-render';
|
||||
|
||||
const initialItems = sortHooks(hooksData.items, 'title');
|
||||
---
|
||||
|
||||
<StarlightPage frontmatter={{ title: 'Hooks', description: 'Automated workflows triggered by Copilot coding agent events', template: 'splash', prev: false, next: false, editUrl: false }}>
|
||||
<div id="main-content" class="hooks-page listing-cards-page">
|
||||
<PageHeader title="Hooks" description="Automated workflows triggered by Copilot coding agent events" icon="hook" />
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="listing-toolbar">
|
||||
<div class="listing-toolbar-row">
|
||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} hooks</div>
|
||||
<details class="listing-controls">
|
||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||
<div class="listing-controls-panel">
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-tag">Tag:</label>
|
||||
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div>
|
||||
<ContributeCTA resourceType="hooks" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
|
||||
<EmbeddedPageData filename="hooks.json" data={hooksData} />
|
||||
<script>
|
||||
import '../scripts/listing-flyouts';
|
||||
import '../scripts/pages/hooks';
|
||||
</script>
|
||||
</StarlightPage>
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||
import Modal from "../components/Modal.astro";
|
||||
import Icon from "../components/Icon.astro";
|
||||
import BackToTop from "../components/BackToTop.astro";
|
||||
|
||||
@@ -116,44 +115,6 @@ const base = import.meta.env.BASE_URL;
|
||||
-
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`${base}hooks/`}
|
||||
class="card card-with-count card-category-automation"
|
||||
id="card-hooks"
|
||||
style="--animation-delay: 150ms;"
|
||||
>
|
||||
<div class="card-icon" aria-hidden="true">
|
||||
<Icon name="hook" size={40} />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>Hooks</h3>
|
||||
<p>Automated workflows triggered by agent events</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="hooks" aria-label="Hook count">
|
||||
-
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`${base}workflows/`}
|
||||
class="card card-with-count card-category-automation"
|
||||
id="card-workflows"
|
||||
style="--animation-delay: 200ms;"
|
||||
>
|
||||
<div class="card-icon" aria-hidden="true">
|
||||
<Icon name="workflow" size={40} />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>Workflows</h3>
|
||||
<p>AI-powered automations for GitHub Actions</p>
|
||||
</div>
|
||||
<div
|
||||
class="card-count"
|
||||
data-count="workflows"
|
||||
aria-label="Workflow count"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`${base}plugins/`}
|
||||
class="card card-with-count card-category-extension"
|
||||
@@ -196,23 +157,6 @@ const base = import.meta.env.BASE_URL;
|
||||
-
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`${base}tools/`}
|
||||
class="card card-with-count card-category-dev"
|
||||
id="card-tools"
|
||||
style="--animation-delay: 350ms;"
|
||||
>
|
||||
<div class="card-icon" aria-hidden="true">
|
||||
<Icon name="wrench" size={40} />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>Tools</h3>
|
||||
<p>MCP servers and developer tools</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="tools" aria-label="Tool count">
|
||||
-
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`${base}learning-hub/`}
|
||||
class="card card-with-count card-category-learn"
|
||||
@@ -232,8 +176,6 @@ const base = import.meta.env.BASE_URL;
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Modal />
|
||||
|
||||
<!-- Back to Top Button -->
|
||||
<BackToTop />
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||
import BackToTop from "../../../../../components/BackToTop.astro";
|
||||
import Breadcrumb from "../../../../../components/pages/Breadcrumb.astro";
|
||||
import FileBrowser from "../../../../../components/pages/FileBrowser.astro";
|
||||
import { readResourceMarkdown } from "../../../../../lib/detail-page";
|
||||
import samplesData from "../../../../../../public/data/samples.json";
|
||||
|
||||
interface RecipeVariant {
|
||||
doc: string;
|
||||
example: string | null;
|
||||
}
|
||||
|
||||
interface Recipe {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
languages: string[];
|
||||
variants: Record<string, RecipeVariant>;
|
||||
external?: boolean;
|
||||
url?: string | null;
|
||||
author?: { name: string; url?: string } | null;
|
||||
}
|
||||
|
||||
interface Language {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
interface Cookbook {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
path: string;
|
||||
featured: boolean;
|
||||
languages: Language[];
|
||||
recipes: Recipe[];
|
||||
}
|
||||
|
||||
interface SamplesData {
|
||||
cookbooks: Cookbook[];
|
||||
}
|
||||
|
||||
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
|
||||
|
||||
function buildCookbookUrl(
|
||||
cookbookId: string,
|
||||
languageId: string,
|
||||
recipeId: string,
|
||||
filePath?: string | null
|
||||
): string {
|
||||
const basePath = `/learning-hub/cookbook/${encodeURIComponent(
|
||||
cookbookId
|
||||
)}/${encodeURIComponent(languageId)}/${encodeURIComponent(recipeId)}/`;
|
||||
if (!filePath) return basePath;
|
||||
return `${basePath}#file=${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
const entries = (samplesData as SamplesData).cookbooks.flatMap((cookbook) =>
|
||||
cookbook.recipes.flatMap((recipe) =>
|
||||
Object.keys(recipe.variants ?? {}).map((languageId) => ({
|
||||
params: {
|
||||
cookbook: cookbook.id,
|
||||
language: languageId,
|
||||
recipe: recipe.id,
|
||||
},
|
||||
}))
|
||||
)
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
const { cookbook, language, recipe } = Astro.params as {
|
||||
cookbook: string;
|
||||
language: string;
|
||||
recipe: string;
|
||||
};
|
||||
|
||||
const cookbookEntry = (samplesData as SamplesData).cookbooks.find(
|
||||
(item) => item.id === cookbook
|
||||
);
|
||||
const recipeEntry = cookbookEntry?.recipes.find((item) => item.id === recipe);
|
||||
const languageEntry = cookbookEntry?.languages.find((item) => item.id === language);
|
||||
const variant = recipeEntry?.variants?.[language];
|
||||
|
||||
if (!cookbookEntry || !recipeEntry || !languageEntry || !variant || recipeEntry.external) {
|
||||
return Astro.redirect("/learning-hub/cookbook/");
|
||||
}
|
||||
|
||||
const { markdownHtml } = readResourceMarkdown(variant.doc);
|
||||
const files = [{ name: variant.doc.split("/").pop() ?? variant.doc, path: variant.doc }];
|
||||
|
||||
if (variant.example) {
|
||||
files.push({
|
||||
name: variant.example.split("/").pop() ?? variant.example,
|
||||
path: variant.example,
|
||||
});
|
||||
}
|
||||
|
||||
const recipeUrl = buildCookbookUrl(cookbook, language, recipe);
|
||||
const exampleUrl = variant.example
|
||||
? buildCookbookUrl(cookbook, language, recipe, variant.example)
|
||||
: null;
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
frontmatter={{
|
||||
title: `${recipeEntry.name} (${languageEntry.name})`,
|
||||
description: recipeEntry.description,
|
||||
template: "splash",
|
||||
prev: false,
|
||||
next: false,
|
||||
editUrl: false,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="main-content"
|
||||
class="resource-detail-page cookbook-detail-page"
|
||||
data-file-browser-page
|
||||
data-bundle-id={`${cookbookEntry.id}-${recipeEntry.id}-${languageEntry.id}`}
|
||||
data-path={variant.doc}
|
||||
>
|
||||
<div class="container">
|
||||
<Breadcrumb
|
||||
paths={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Learning Hub", href: "/learning-hub/" },
|
||||
{ name: "Cookbook", href: "/learning-hub/cookbook/" },
|
||||
{ name: cookbookEntry.name, href: "/learning-hub/cookbook/" },
|
||||
]}
|
||||
title={recipeEntry.name}
|
||||
/>
|
||||
|
||||
<header class="cookbook-detail-header">
|
||||
<h1>{recipeEntry.name}</h1>
|
||||
<p>{recipeEntry.description}</p>
|
||||
<div class="cookbook-detail-meta">
|
||||
<span class="cookbook-detail-language">{languageEntry.icon} {languageEntry.name}</span>
|
||||
<div class="cookbook-detail-tags">
|
||||
{recipeEntry.tags.map((tag) => (
|
||||
<span class="recipe-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cookbook-detail-actions">
|
||||
<a class="btn btn-secondary btn-small" href={recipeUrl}>
|
||||
View Recipe
|
||||
</a>
|
||||
{exampleUrl && (
|
||||
<a class="btn btn-secondary btn-small" href={exampleUrl}>
|
||||
View Example
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
class="btn btn-secondary btn-small"
|
||||
href={`https://github.com/github/awesome-copilot/blob/main/${variant.doc}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<FileBrowser
|
||||
files={files}
|
||||
primaryPath={variant.doc}
|
||||
primaryName={variant.doc.split("/").pop() ?? variant.doc}
|
||||
primaryHtml={markdownHtml}
|
||||
githubBase={GITHUB_BASE}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
|
||||
<script>
|
||||
import "../../../../../scripts/pages/file-browser";
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
.cookbook-detail-header {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cookbook-detail-header h1 {
|
||||
margin: 0;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.cookbook-detail-header p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cookbook-detail-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cookbook-detail-language {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: "Monaspace Argon NF", monospace;
|
||||
}
|
||||
|
||||
.cookbook-detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cookbook-detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</StarlightPage>
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import samplesData from '../../../../public/data/samples.json';
|
||||
import Modal from '../../../components/Modal.astro';
|
||||
import EmbeddedPageData from '../../../components/EmbeddedPageData.astro';
|
||||
import {
|
||||
getRecipeResultsCountText,
|
||||
@@ -56,7 +55,6 @@ const languageOptions = Array.from(
|
||||
<div id="samples-list" set:html={renderCookbookSectionsHtml(initialRecipeMatches)}></div>
|
||||
</div>
|
||||
|
||||
<Modal />
|
||||
<EmbeddedPageData filename="samples.json" data={samplesData} />
|
||||
|
||||
<style is:global>
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import toolsData from '../../public/data/tools.json';
|
||||
import Modal from '../components/Modal.astro';
|
||||
import ContributeCTA from "../components/ContributeCTA.astro";
|
||||
import EmbeddedPageData from "../components/EmbeddedPageData.astro";
|
||||
import PageHeader from "../components/PageHeader.astro";
|
||||
import BackToTop from '../components/BackToTop.astro';
|
||||
import { renderToolsHtml, sortTools } from "../scripts/pages/tools-render";
|
||||
|
||||
const initialItems = sortTools(
|
||||
toolsData.items.map((item) => ({
|
||||
...item,
|
||||
title: item.name,
|
||||
})),
|
||||
"title"
|
||||
);
|
||||
---
|
||||
|
||||
<StarlightPage frontmatter={{ title: 'Tools', description: 'MCP servers and developer tools for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
|
||||
<div id="main-content">
|
||||
<PageHeader title="Tools" description="MCP servers and developer tools for GitHub Copilot" icon="wrench" />
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="listing-toolbar">
|
||||
<div class="listing-toolbar-row">
|
||||
<div id="results-count" class="results-count" aria-live="polite">{initialItems.length} tools</div>
|
||||
<details class="listing-controls">
|
||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||
<div class="listing-controls-panel">
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-category">Category:</label>
|
||||
<select id="filter-category" class="filter-select" aria-label="Filter by category">
|
||||
<option value="">All Categories</option>
|
||||
{toolsData.filters.categories.map((category) => (
|
||||
<option value={category}>{category}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" class="filter-select" aria-label="Sort tools">
|
||||
<option value="featured">Featured First</option>
|
||||
<option value="title">Title</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tools-list" set:html={renderToolsHtml(initialItems)}></div>
|
||||
|
||||
<div class="coming-soon">
|
||||
<h2>More Tools Coming Soon</h2>
|
||||
<p>
|
||||
We're working on additional tools to enhance your GitHub Copilot
|
||||
experience. Check back soon!
|
||||
</p>
|
||||
</div>
|
||||
<ContributeCTA resourceType="tools" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal />
|
||||
<BackToTop />
|
||||
<EmbeddedPageData filename="tools.json" data={toolsData} />
|
||||
|
||||
<style is:global>
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
background-color: var(--color-card-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tool-badge.featured {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tool-badge.category {
|
||||
background-color: var(--color-purple-light);
|
||||
color: var(--color-purple-dark);
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tool-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tool-section h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-section ul {
|
||||
margin: 0;
|
||||
padding-left: 24px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.tool-section li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tool-config {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tool-config h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-emphasis);
|
||||
}
|
||||
|
||||
.tool-config-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool-config pre {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tool-config code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.coming-soon h2 {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.coming-soon p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.copy-config-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
background-color: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copy-config-btn:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.copy-config-btn.copied {
|
||||
background-color: var(--color-success);
|
||||
color: white;
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
/* Search highlight */
|
||||
.search-highlight {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-text-emphasis);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import '../scripts/listing-flyouts';
|
||||
import '../scripts/pages/tools';
|
||||
</script>
|
||||
</StarlightPage>
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||
import BackToTop from "../../components/BackToTop.astro";
|
||||
import workflowsData from "../../../public/data/workflows.json";
|
||||
import RawMarkdown from "../../components/pages/RawMarkdown.astro";
|
||||
import type { HeaderItem } from "../../components/pages/Header.astro";
|
||||
import DetailHeader from "../../components/pages/Header.astro";
|
||||
import Breadcrumb from "../../components/pages/Breadcrumb.astro";
|
||||
import Main from "../../components/pages/Main.astro";
|
||||
import Sidebar from "../../components/pages/Sidebar.astro";
|
||||
import { readResourceMarkdown, formatLastUpdated } from "../../lib/detail-page";
|
||||
|
||||
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
|
||||
const workflowGuideHref = "/learning-hub/agentic-workflows/";
|
||||
|
||||
interface WorkflowItem extends HeaderItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
triggers: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
return (workflowsData.items as WorkflowItem[]).map((item) => ({
|
||||
params: { id: item.id },
|
||||
props: { item },
|
||||
}));
|
||||
}
|
||||
|
||||
const { item } = Astro.props as { item: WorkflowItem };
|
||||
|
||||
const { markdownHtml, frontmatterText, rawMarkdown } = readResourceMarkdown(
|
||||
item.path
|
||||
);
|
||||
const lastUpdated = formatLastUpdated(item.lastUpdated);
|
||||
const githubUrl = `${GITHUB_BASE}/${item.path}`;
|
||||
|
||||
const chips: { title: string; items: string[] }[] = [];
|
||||
if (item.triggers.length > 0) {
|
||||
chips.push({ title: "Triggers", items: item.triggers });
|
||||
}
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
frontmatter={{
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
template: "splash",
|
||||
prev: false,
|
||||
next: false,
|
||||
editUrl: false,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="main-content"
|
||||
class="resource-detail-page"
|
||||
data-resource-detail
|
||||
data-path={item.path}
|
||||
>
|
||||
<RawMarkdown markdown={rawMarkdown} />
|
||||
<div class="container">
|
||||
<Breadcrumb
|
||||
paths={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Agentic Workflows", href: "/workflows/" },
|
||||
]}
|
||||
title={item.title}
|
||||
/>
|
||||
|
||||
<DetailHeader item={item} />
|
||||
|
||||
<div class="detail-layout">
|
||||
<Main markdownHtml={markdownHtml} githubUrl={githubUrl} />
|
||||
|
||||
<Sidebar
|
||||
item={item}
|
||||
lastUpdated={lastUpdated ?? undefined}
|
||||
frontmatterText={frontmatterText || undefined}
|
||||
githubUrl={githubUrl}
|
||||
chips={chips}
|
||||
>
|
||||
<div class="skill-install" slot="install">
|
||||
<p class="skill-install-label">Install this workflow</p>
|
||||
<p class="skill-install-note">
|
||||
Install the <code>gh aw</code> CLI{" "}
|
||||
(<code>gh extension install github/gh-aw</code>), then copy this
|
||||
file into <code>.github/workflows/</code> and run{" "}
|
||||
<code>gh aw compile</code> to generate the lock file. See the{" "}
|
||||
<a href={workflowGuideHref}>Agentic Workflows guide</a>{" "}
|
||||
for full setup steps.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary skill-download-btn"
|
||||
data-action="download"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
><path
|
||||
d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"
|
||||
></path><path
|
||||
d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"
|
||||
></path></svg
|
||||
>
|
||||
Download workflow
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary skill-download-btn"
|
||||
data-action="copy-markdown"
|
||||
>
|
||||
Copy markdown
|
||||
</button>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
|
||||
<script>
|
||||
import "../../scripts/pages/resource-detail";
|
||||
</script>
|
||||
</StarlightPage>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import workflowsData from '../../public/data/workflows.json';
|
||||
import ContributeCTA from '../components/ContributeCTA.astro';
|
||||
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
import BackToTop from '../components/BackToTop.astro';
|
||||
import { renderWorkflowsHtml, sortWorkflows } from '../scripts/pages/workflows-render';
|
||||
|
||||
const initialItems = sortWorkflows(workflowsData.items, 'title');
|
||||
---
|
||||
|
||||
<StarlightPage frontmatter={{ title: 'Agentic Workflows', description: 'AI-powered repository automations that run coding agents in GitHub Actions', template: 'splash', prev: false, next: false, editUrl: false }}>
|
||||
<div id="main-content" class="workflows-page listing-cards-page">
|
||||
<PageHeader title="Agentic Workflows" description="AI-powered repository automations that run coding agents in GitHub Actions" icon="workflow" />
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="listing-toolbar">
|
||||
<div class="listing-toolbar-row">
|
||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} workflows</div>
|
||||
<details class="listing-controls">
|
||||
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||
<div class="listing-controls-panel">
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filter-trigger">Trigger:</label>
|
||||
<select id="filter-trigger" multiple aria-label="Filter by trigger"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div>
|
||||
<ContributeCTA resourceType="workflows" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
<EmbeddedPageData filename="workflows.json" data={workflowsData} />
|
||||
|
||||
<script>
|
||||
import '../scripts/listing-flyouts';
|
||||
import '../scripts/pages/workflows';
|
||||
</script>
|
||||
</StarlightPage>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,7 @@ export interface RenderableExtension {
|
||||
installCommand?: string | null;
|
||||
installUrl?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
externalSource?: string | null;
|
||||
external?: boolean;
|
||||
author?: { name: string; url?: string } | null;
|
||||
}
|
||||
@@ -95,13 +96,20 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
const sourceUrl = safeUrl(
|
||||
item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "")
|
||||
);
|
||||
const externalSource = (item.externalSource || "").trim();
|
||||
const pluginId = item.pluginName || item.id;
|
||||
const ghappInstallUrl =
|
||||
!item.external && pluginId
|
||||
? `ghapp://plugins/install?source=${encodeURIComponent(
|
||||
`${pluginId}@awesome-copilot`
|
||||
)}`
|
||||
: "";
|
||||
item.external
|
||||
? externalSource
|
||||
? `ghapp://plugins/marketplace/add?source=${encodeURIComponent(
|
||||
externalSource
|
||||
)}`
|
||||
: ""
|
||||
: pluginId
|
||||
? `ghapp://plugins/install?source=${encodeURIComponent(
|
||||
`${pluginId}@awesome-copilot`
|
||||
)}`
|
||||
: "";
|
||||
|
||||
const previewImageUrl = safeUrl(item.imageUrl);
|
||||
const previewMediaHtml = previewImageUrl
|
||||
@@ -152,14 +160,18 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
<button
|
||||
${
|
||||
!item.external
|
||||
? `<button
|
||||
class="btn btn-secondary btn-small copy-install-url-btn"
|
||||
data-install-url="${escapeHtml(installUrl)}"
|
||||
title="Copy fallback URL install target"
|
||||
${installUrl ? "" : "disabled"}
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
</button>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
sourceUrl
|
||||
? `<a href="${escapeHtml(
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import {
|
||||
escapeHtml,
|
||||
getGitHubUrl,
|
||||
getLastUpdatedHtml,
|
||||
} from "../utils";
|
||||
import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render";
|
||||
|
||||
export interface RenderableHookFile {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RenderableHook {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
readmeFile: string;
|
||||
hooks: string[];
|
||||
tags: string[];
|
||||
assets: string[];
|
||||
files: RenderableHookFile[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export type HookSortOption = "title" | "lastUpdated";
|
||||
|
||||
/**
|
||||
* Build the URL of a hook's dedicated detail page.
|
||||
*/
|
||||
export function getHookDetailUrl(id: string): string {
|
||||
return `/hook/${id}/`;
|
||||
}
|
||||
|
||||
export function sortHooks<T extends RenderableHook>(
|
||||
items: T[],
|
||||
sort: HookSortOption
|
||||
): T[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (sort === "lastUpdated") {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderHooksHtml(items: RenderableHook[]): string {
|
||||
if (items.length === 0) {
|
||||
return renderEmptyStateHtml("No hooks found", "Try adjusting the selected filters.");
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const metaHtml = `
|
||||
${item.hooks
|
||||
.map(
|
||||
(hook) => `<span class="resource-tag tag-hook">${escapeHtml(hook)}</span>`
|
||||
)
|
||||
.join("")}
|
||||
${item.tags
|
||||
.map((tag) => `<span class="resource-tag tag-tag">${escapeHtml(tag)}</span>`)
|
||||
.join("")}
|
||||
${
|
||||
item.assets.length > 0
|
||||
? `<span class="resource-tag tag-assets">${item.assets.length} asset${
|
||||
item.assets.length === 1 ? "" : "s"
|
||||
}</span>`
|
||||
: ""
|
||||
}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
`;
|
||||
|
||||
const actionsHtml = `
|
||||
<button class="btn btn-primary download-hook-btn" data-hook-id="${escapeHtml(
|
||||
item.id
|
||||
)}" title="Download as ZIP">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
|
||||
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<a href="${getGitHubUrl(
|
||||
item.path
|
||||
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
`;
|
||||
|
||||
return renderSharedCardHtml({
|
||||
title: item.title,
|
||||
description: item.description || "No description",
|
||||
href: getHookDetailUrl(item.id),
|
||||
articleAttributes: {
|
||||
"data-path": item.readmeFile,
|
||||
"data-hook-id": item.id,
|
||||
},
|
||||
metaHtml,
|
||||
actionsHtml,
|
||||
});
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* Hooks page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
showToast,
|
||||
downloadZipBundle,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
|
||||
import {
|
||||
renderHooksHtml,
|
||||
sortHooks,
|
||||
type HookSortOption,
|
||||
type RenderableHook,
|
||||
} from './hooks-render';
|
||||
|
||||
interface Hook extends RenderableHook {}
|
||||
|
||||
interface HooksData {
|
||||
items: Hook[];
|
||||
filters: {
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Hook[] = [];
|
||||
let tagSelectEl: HTMLSelectElement | null = null;
|
||||
let currentFilters = {
|
||||
tags: [] as string[],
|
||||
};
|
||||
let currentSort: HookSortOption = 'title';
|
||||
let resourceListHandlersReady = false;
|
||||
|
||||
function sortItems(items: Hook[]): Hook[] {
|
||||
return sortHooks(items, currentSort);
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById('results-count');
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.tags.length > 0) {
|
||||
results = results.filter((item) => item.tags.some((tag) => currentFilters.tags.includes(tag)));
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results);
|
||||
let countText = `${results.length} hook${results.length === 1 ? '' : 's'}`;
|
||||
if (currentFilters.tags.length > 0) {
|
||||
countText = `${results.length} of ${allItems.length} hooks (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Hook[]): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = renderHooksHtml(items);
|
||||
}
|
||||
|
||||
async function downloadHook(hookId: string, btn: HTMLButtonElement): Promise<void> {
|
||||
const hook = allItems.find((item) => item.id === hookId);
|
||||
if (!hook) {
|
||||
showToast('Hook not found.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = [
|
||||
{ name: 'README.md', path: hook.readmeFile },
|
||||
...hook.assets.map((asset) => ({
|
||||
name: asset,
|
||||
path: `${hook.path}/${asset}`,
|
||||
})),
|
||||
];
|
||||
|
||||
if (files.length === 0) {
|
||||
showToast('No files found for this hook.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalContent = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
|
||||
|
||||
try {
|
||||
await downloadZipBundle(hook.id, files);
|
||||
|
||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Download failed.';
|
||||
showToast(message, 'error');
|
||||
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||
if (!list || resourceListHandlersReady) return;
|
||||
|
||||
list.addEventListener('click', (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const downloadButton = target.closest('.download-hook-btn') as HTMLButtonElement | null;
|
||||
if (downloadButton) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const hookId = downloadButton.dataset.hookId;
|
||||
if (hookId) downloadHook(hookId, downloadButton);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
resourceListHandlersReady = true;
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: '',
|
||||
hook: [],
|
||||
tag: currentFilters.tags,
|
||||
sort: currentSort === 'title' ? '' : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initHooksPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
|
||||
|
||||
setupResourceListHandlers(list as HTMLElement | null);
|
||||
|
||||
const data = await fetchData<HooksData>('hooks.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
tagSelectEl = document.getElementById('filter-tag') as HTMLSelectElement | null;
|
||||
if (tagSelectEl) {
|
||||
tagSelectEl.innerHTML = '';
|
||||
data.filters.tags.forEach((tag) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tag;
|
||||
option.textContent = tag;
|
||||
tagSelectEl?.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
const initialTags = getQueryParamValues('tag').filter((tag) => data.filters.tags.includes(tag));
|
||||
const initialSort = getQueryParam('sort');
|
||||
|
||||
if (initialTags.length > 0) {
|
||||
currentFilters.tags = initialTags;
|
||||
setSelectValues(tagSelectEl, initialTags);
|
||||
}
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
tagSelectEl?.addEventListener('change', () => {
|
||||
currentFilters.tags = getSelectValues(tagSelectEl);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as HookSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { tags: [] };
|
||||
currentSort = 'title';
|
||||
clearSelectValues(tagSelectEl);
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initHooksPage);
|
||||
@@ -40,6 +40,19 @@ export interface CookbookRecipeMatch {
|
||||
highlightedName?: string;
|
||||
}
|
||||
|
||||
function buildCookbookRecipeUrl(
|
||||
cookbookId: string,
|
||||
languageId: string,
|
||||
recipeId: string,
|
||||
filePath?: string | null
|
||||
): string {
|
||||
const basePath = `/learning-hub/cookbook/${encodeURIComponent(
|
||||
cookbookId
|
||||
)}/${encodeURIComponent(languageId)}/${encodeURIComponent(recipeId)}/`;
|
||||
if (!filePath) return basePath;
|
||||
return `${basePath}#file=${encodeURIComponent(filePath)}`;
|
||||
}
|
||||
|
||||
export function getRecipeResultsCountText(
|
||||
filteredCount: number,
|
||||
totalCount: number
|
||||
@@ -211,27 +224,32 @@ function renderRecipeCard(
|
||||
${
|
||||
variant
|
||||
? `
|
||||
<button class="btn btn-secondary btn-small view-recipe-btn" data-doc="${escapeHtml(
|
||||
variant.doc
|
||||
<a class="btn btn-secondary btn-small" href="${buildCookbookRecipeUrl(
|
||||
cookbook.id,
|
||||
displayLanguage,
|
||||
recipe.id
|
||||
)}">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
|
||||
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5z"/>
|
||||
</svg>
|
||||
View Recipe
|
||||
</button>
|
||||
</a>
|
||||
${
|
||||
variant.example
|
||||
? `
|
||||
<button class="btn btn-secondary btn-small view-example-btn" data-example="${escapeHtml(
|
||||
variant.example
|
||||
? `
|
||||
<a class="btn btn-secondary btn-small" href="${buildCookbookRecipeUrl(
|
||||
cookbook.id,
|
||||
displayLanguage,
|
||||
recipe.id,
|
||||
variant.example
|
||||
)}">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
|
||||
</svg>
|
||||
View Example
|
||||
</button>
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4.72 3.22a.75.75 0 0 1 1.06 0l3.5 3.5a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06-1.06L7.69 7.5 4.72 4.28a.75.75 0 0 1 0-1.06zm6.25 1.06L10.22 5l.75.75-2.25 2.25 2.25 2.25-.75.75-.75-.72L11.97 7.5z"/>
|
||||
</svg>
|
||||
View Example
|
||||
</a>
|
||||
`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
<a href="https://github.com/github/awesome-copilot/blob/main/${escapeHtml(
|
||||
variant.doc
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { FuzzySearch, type SearchableItem } from "../search";
|
||||
import { fetchData, debounce } from "../utils";
|
||||
import { createChoices, getChoicesValues, type Choices } from "../choices";
|
||||
import { setupModal } from "../modal";
|
||||
import {
|
||||
getRecipeResultsCountText,
|
||||
renderCookbookSectionsHtml,
|
||||
@@ -62,10 +61,10 @@ export async function initSamplesPage(): Promise<void> {
|
||||
search = new FuzzySearch(allRecipes);
|
||||
|
||||
// Setup UI
|
||||
setupModal();
|
||||
handleLegacyCookbookHashLink();
|
||||
setupFilters();
|
||||
setupSearch();
|
||||
setupRecipeListeners();
|
||||
setupInteractionListeners();
|
||||
updateResultsCount();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize samples page:", error);
|
||||
@@ -259,38 +258,16 @@ function renderCookbooks(): void {
|
||||
});
|
||||
|
||||
// Setup event listeners
|
||||
setupRecipeListeners();
|
||||
setupInteractionListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for recipe interactions
|
||||
* Setup event listeners for cookbook interactions
|
||||
*/
|
||||
function setupRecipeListeners(): void {
|
||||
// View recipe buttons
|
||||
document.querySelectorAll(".view-recipe-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const docPath = (btn as HTMLElement).dataset.doc;
|
||||
if (docPath) {
|
||||
await showRecipeContent(docPath, "recipe");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// View example buttons
|
||||
document.querySelectorAll(".view-example-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const examplePath = (btn as HTMLElement).dataset.example;
|
||||
if (examplePath) {
|
||||
await showRecipeContent(examplePath, "example");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setupInteractionListeners(): void {
|
||||
// Language tab clicks
|
||||
document.querySelectorAll(".lang-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", (e) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const langId = (tab as HTMLElement).dataset.lang;
|
||||
if (langId) {
|
||||
selectedLanguage = langId;
|
||||
@@ -307,15 +284,60 @@ function setupRecipeListeners(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show recipe/example content in modal
|
||||
* Redirect legacy cookbook #file=<path> links to canonical cookbook routes.
|
||||
*/
|
||||
async function showRecipeContent(
|
||||
filePath: string,
|
||||
type: "recipe" | "example"
|
||||
): Promise<void> {
|
||||
// Use existing modal infrastructure
|
||||
const { openFileModal } = await import("../modal");
|
||||
await openFileModal(filePath, type);
|
||||
function handleLegacyCookbookHashLink(): void {
|
||||
const hashMatch = window.location.hash.match(/^#file=(.+)$/);
|
||||
if (!hashMatch) return;
|
||||
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = decodeURIComponent(hashMatch[1]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseCookbookPath(filePath);
|
||||
if (!parsed) return;
|
||||
|
||||
const nextUrl = `/learning-hub/cookbook/${encodeURIComponent(
|
||||
parsed.cookbook
|
||||
)}/${encodeURIComponent(parsed.language)}/${encodeURIComponent(
|
||||
parsed.recipe
|
||||
)}/${parsed.example ? `#file=${encodeURIComponent(filePath)}` : ""}`;
|
||||
|
||||
window.location.replace(nextUrl);
|
||||
}
|
||||
|
||||
function parseCookbookPath(filePath: string): {
|
||||
cookbook: string;
|
||||
language: string;
|
||||
recipe: string;
|
||||
example: boolean;
|
||||
} | null {
|
||||
const docMatch = filePath.match(/^cookbook\/([^/]+)\/([^/]+)\/([^/]+)\.md$/);
|
||||
if (docMatch) {
|
||||
return {
|
||||
cookbook: docMatch[1],
|
||||
language: docMatch[2],
|
||||
recipe: docMatch[3],
|
||||
example: false,
|
||||
};
|
||||
}
|
||||
|
||||
const exampleMatch = filePath.match(
|
||||
/^cookbook\/([^/]+)\/([^/]+)\/recipe\/([^/.]+)\.[^/.]+$/
|
||||
);
|
||||
if (exampleMatch) {
|
||||
return {
|
||||
cookbook: exampleMatch[1],
|
||||
language: exampleMatch[2],
|
||||
recipe: exampleMatch[3],
|
||||
example: true,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import { escapeHtml } from "../utils";
|
||||
|
||||
export interface RenderableTool {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
featured: boolean;
|
||||
requirements: string[];
|
||||
features: string[];
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
};
|
||||
configuration?: {
|
||||
type: string;
|
||||
content: string;
|
||||
} | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export type ToolSortOption = "featured" | "title";
|
||||
|
||||
export function sortTools<T extends RenderableTool>(
|
||||
tools: T[],
|
||||
sort: ToolSortOption
|
||||
): T[] {
|
||||
return [...tools].sort((a, b) => {
|
||||
if (sort === "featured") {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function formatMultilineText(text: string): string {
|
||||
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||
}
|
||||
|
||||
function sanitizeToolUrl(url: string): string {
|
||||
try {
|
||||
const protocol = new URL(url).protocol;
|
||||
if (
|
||||
protocol === "http:" ||
|
||||
protocol === "https:" ||
|
||||
protocol === "vscode:" ||
|
||||
protocol === "vscode-insiders:"
|
||||
) {
|
||||
return escapeHtml(url);
|
||||
}
|
||||
} catch {
|
||||
return "#";
|
||||
}
|
||||
|
||||
return "#";
|
||||
}
|
||||
|
||||
function getToolActionLink(
|
||||
href: string | undefined,
|
||||
label: string,
|
||||
className: string
|
||||
): string {
|
||||
if (!href) return "";
|
||||
return `<a href="${sanitizeToolUrl(
|
||||
href
|
||||
)}" class="${className}" target="_blank" rel="noopener">${label}</a>`;
|
||||
}
|
||||
|
||||
export function renderToolsHtml(
|
||||
tools: RenderableTool[]
|
||||
): string {
|
||||
if (tools.length === 0) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<h3>No tools found</h3>
|
||||
<p>Try a different category or clear the current filters</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return tools
|
||||
.map((tool) => {
|
||||
const badges: string[] = [];
|
||||
if (tool.featured) {
|
||||
badges.push('<span class="tool-badge featured">Featured</span>');
|
||||
}
|
||||
badges.push(
|
||||
`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`
|
||||
);
|
||||
|
||||
const features =
|
||||
tool.features && tool.features.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Features</h3>
|
||||
<ul>${tool.features
|
||||
.map((feature) => `<li>${escapeHtml(feature)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const requirements =
|
||||
tool.requirements && tool.requirements.length > 0
|
||||
? `<div class="tool-section">
|
||||
<h3>Requirements</h3>
|
||||
<ul>${tool.requirements
|
||||
.map((requirement) => `<li>${escapeHtml(requirement)}</li>`)
|
||||
.join("")}</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const tags =
|
||||
tool.tags && tool.tags.length > 0
|
||||
? `<div class="tool-tags">
|
||||
${tool.tags
|
||||
.map((tag) => `<span class="tool-tag">${escapeHtml(tag)}</span>`)
|
||||
.join("")}
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const config = tool.configuration
|
||||
? `<div class="tool-config">
|
||||
<h3>Configuration</h3>
|
||||
<div class="tool-config-wrapper">
|
||||
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
|
||||
</div>
|
||||
<button class="copy-config-btn" data-config="${encodeURIComponent(
|
||||
tool.configuration.content
|
||||
)}">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
|
||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
|
||||
</svg>
|
||||
Copy Configuration
|
||||
</button>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const actions = [
|
||||
getToolActionLink(tool.links.blog, "📖 Blog", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.marketplace,
|
||||
"🏪 Marketplace",
|
||||
"btn btn-secondary"
|
||||
),
|
||||
getToolActionLink(tool.links.npm, "📦 npm", "btn btn-secondary"),
|
||||
getToolActionLink(tool.links.pypi, "🐍 PyPI", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.documentation,
|
||||
"📚 Docs",
|
||||
"btn btn-secondary"
|
||||
),
|
||||
getToolActionLink(tool.links.github, "GitHub", "btn btn-secondary"),
|
||||
getToolActionLink(
|
||||
tool.links.vscode,
|
||||
"Install in VS Code",
|
||||
"btn btn-primary"
|
||||
),
|
||||
getToolActionLink(
|
||||
tool.links["vscode-insiders"],
|
||||
"VS Code Insiders",
|
||||
"btn btn-outline"
|
||||
),
|
||||
getToolActionLink(
|
||||
tool.links["visual-studio"],
|
||||
"Visual Studio",
|
||||
"btn btn-outline"
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
const actionsHtml =
|
||||
actions.length > 0
|
||||
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="tool-card">
|
||||
<div class="tool-header">
|
||||
<h2>${escapeHtml(tool.name)}</h2>
|
||||
<div class="tool-badges">
|
||||
${badges.join("")}
|
||||
</div>
|
||||
</div>
|
||||
<p class="tool-description">${formatMultilineText(tool.description)}</p>
|
||||
${features}
|
||||
${requirements}
|
||||
${config}
|
||||
${tags}
|
||||
${actionsHtml}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/**
|
||||
* Tools page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
import {
|
||||
renderToolsHtml,
|
||||
sortTools,
|
||||
type ToolSortOption,
|
||||
} from "./tools-render";
|
||||
|
||||
export interface Tool {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
featured: boolean;
|
||||
requirements: string[];
|
||||
features: string[];
|
||||
links: {
|
||||
blog?: string;
|
||||
vscode?: string;
|
||||
"vscode-insiders"?: string;
|
||||
"visual-studio"?: string;
|
||||
github?: string;
|
||||
documentation?: string;
|
||||
marketplace?: string;
|
||||
npm?: string;
|
||||
pypi?: string;
|
||||
};
|
||||
configuration?: {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface ToolsData {
|
||||
items: Tool[];
|
||||
filters: {
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Tool[] = [];
|
||||
let currentFilters = {
|
||||
categories: [] as string[],
|
||||
};
|
||||
let currentSort: ToolSortOption = "featured";
|
||||
let copyHandlersReady = false;
|
||||
let initialized = false;
|
||||
|
||||
function sortItems(items: Tool[]): Tool[] {
|
||||
return sortTools(items, currentSort);
|
||||
}
|
||||
|
||||
function getCountText(resultsCount: number): string {
|
||||
if (currentFilters.categories.length === 0) {
|
||||
return `${resultsCount} tool${resultsCount === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
return `${resultsCount} of ${allItems.length} tools (filtered by ${currentFilters.categories.length} categor${currentFilters.categories.length === 1 ? "y" : "ies"})`;
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById("results-count");
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.categories.length > 0) {
|
||||
results = results.filter((item) =>
|
||||
currentFilters.categories.includes(item.category)
|
||||
);
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderTools(results);
|
||||
if (countEl) countEl.textContent = getCountText(results.length);
|
||||
}
|
||||
|
||||
function renderTools(tools: Tool[]): void {
|
||||
const container = document.getElementById("tools-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = renderToolsHtml(tools);
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: "",
|
||||
category: currentFilters.categories,
|
||||
sort: currentSort === "featured" ? "" : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
function setupCopyConfigHandlers(): void {
|
||||
if (copyHandlersReady) return;
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const button = (event.target as HTMLElement).closest(
|
||||
".copy-config-btn"
|
||||
) as HTMLButtonElement | null;
|
||||
if (!button) return;
|
||||
|
||||
event.stopPropagation();
|
||||
const config = decodeURIComponent(button.dataset.config || "");
|
||||
try {
|
||||
await navigator.clipboard.writeText(config);
|
||||
button.classList.add("copied");
|
||||
const originalHtml = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
|
||||
</svg>
|
||||
Copied!
|
||||
`;
|
||||
setTimeout(() => {
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = originalHtml;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
});
|
||||
|
||||
copyHandlersReady = true;
|
||||
}
|
||||
|
||||
export async function initToolsPage(): Promise<void> {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const categoryFilter = document.getElementById(
|
||||
"filter-category"
|
||||
) as HTMLSelectElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<ToolsData>("tools.json");
|
||||
if (!data || !data.items) {
|
||||
const container = document.getElementById("tools-list");
|
||||
if (container)
|
||||
container.innerHTML =
|
||||
'<div class="empty-state"><h3>Failed to load tools</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Map items to include title for FuzzySearch
|
||||
allItems = data.items.map((item) => ({
|
||||
...item,
|
||||
title: item.name, // FuzzySearch uses title
|
||||
}));
|
||||
|
||||
// Populate category filter
|
||||
if (categoryFilter && data.filters.categories) {
|
||||
categoryFilter.innerHTML =
|
||||
'<option value="">All Categories</option>' +
|
||||
data.filters.categories
|
||||
.map(
|
||||
(c) => `<option value="${c}">${c}</option>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const initialCategory = getQueryParam("category");
|
||||
if (initialCategory && data.filters.categories.includes(initialCategory)) {
|
||||
currentFilters.categories = [initialCategory];
|
||||
categoryFilter.value = initialCategory;
|
||||
}
|
||||
|
||||
categoryFilter.addEventListener("change", () => {
|
||||
currentFilters.categories = categoryFilter.value
|
||||
? [categoryFilter.value]
|
||||
: [];
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
}
|
||||
|
||||
const initialSort = getQueryParam("sort");
|
||||
if (initialSort === "title") {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as ToolSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
|
||||
// Clear filters
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [] };
|
||||
currentSort = "featured";
|
||||
if (categoryFilter) categoryFilter.value = "";
|
||||
if (sortSelect) sortSelect.value = "featured";
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
setupCopyConfigHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", initToolsPage);
|
||||
@@ -1,75 +0,0 @@
|
||||
import {
|
||||
escapeHtml,
|
||||
getActionButtonsHtml,
|
||||
getGitHubUrl,
|
||||
getLastUpdatedHtml,
|
||||
} from '../utils';
|
||||
import { renderEmptyStateHtml, renderSharedCardHtml } from './card-render';
|
||||
|
||||
export interface RenderableWorkflow {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
triggers: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export type WorkflowSortOption = 'title' | 'lastUpdated';
|
||||
|
||||
/**
|
||||
* Build the URL of a workflow's dedicated detail page.
|
||||
*/
|
||||
export function getWorkflowDetailUrl(id: string): string {
|
||||
return `/workflow/${id}/`;
|
||||
}
|
||||
|
||||
export function sortWorkflows<T extends RenderableWorkflow>(
|
||||
items: T[],
|
||||
sort: WorkflowSortOption
|
||||
): T[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (sort === 'lastUpdated') {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderWorkflowsHtml(
|
||||
items: RenderableWorkflow[]
|
||||
): string {
|
||||
if (items.length === 0) {
|
||||
return renderEmptyStateHtml('No workflows found', 'Try adjusting the selected filters.');
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const metaHtml = `
|
||||
${item.triggers
|
||||
.map((trigger) => `<span class="resource-tag tag-trigger">${escapeHtml(trigger)}</span>`)
|
||||
.join('')}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
`;
|
||||
|
||||
const actionsHtml = `
|
||||
${getActionButtonsHtml(item.path)}
|
||||
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
|
||||
`;
|
||||
|
||||
return renderSharedCardHtml({
|
||||
title: item.title,
|
||||
description: item.description || 'No description',
|
||||
href: getWorkflowDetailUrl(item.id),
|
||||
articleAttributes: {
|
||||
'data-path': item.path,
|
||||
},
|
||||
metaHtml,
|
||||
actionsHtml,
|
||||
});
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Workflows page functionality
|
||||
*/
|
||||
import {
|
||||
fetchData,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
setupActionHandlers,
|
||||
updateQueryParams,
|
||||
} from '../utils';
|
||||
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
|
||||
import {
|
||||
renderWorkflowsHtml,
|
||||
sortWorkflows,
|
||||
type RenderableWorkflow,
|
||||
type WorkflowSortOption,
|
||||
} from './workflows-render';
|
||||
|
||||
interface Workflow extends RenderableWorkflow {
|
||||
id: string;
|
||||
path: string;
|
||||
triggers: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface WorkflowsData {
|
||||
items: Workflow[];
|
||||
filters: {
|
||||
triggers: string[];
|
||||
};
|
||||
}
|
||||
|
||||
let allItems: Workflow[] = [];
|
||||
let triggerSelectEl: HTMLSelectElement | null = null;
|
||||
let currentFilters = {
|
||||
triggers: [] as string[],
|
||||
};
|
||||
let currentSort: WorkflowSortOption = 'title';
|
||||
|
||||
function sortItems(items: Workflow[]): Workflow[] {
|
||||
return sortWorkflows(items, currentSort);
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const countEl = document.getElementById('results-count');
|
||||
let results = [...allItems];
|
||||
|
||||
if (currentFilters.triggers.length > 0) {
|
||||
results = results.filter((item) => item.triggers.some((trigger) => currentFilters.triggers.includes(trigger)));
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results);
|
||||
let countText = `${results.length} workflow${results.length === 1 ? '' : 's'}`;
|
||||
if (currentFilters.triggers.length > 0) {
|
||||
countText = `${results.length} of ${allItems.length} workflows (filtered by ${currentFilters.triggers.length} trigger${currentFilters.triggers.length > 1 ? 's' : ''})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Workflow[]): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = renderWorkflowsHtml(items);
|
||||
}
|
||||
|
||||
function syncUrlState(): void {
|
||||
updateQueryParams({
|
||||
q: '',
|
||||
trigger: currentFilters.triggers,
|
||||
sort: currentSort === 'title' ? '' : currentSort,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initWorkflowsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
|
||||
|
||||
const data = await fetchData<WorkflowsData>('workflows.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
triggerSelectEl = document.getElementById('filter-trigger') as HTMLSelectElement | null;
|
||||
if (triggerSelectEl) {
|
||||
triggerSelectEl.innerHTML = '';
|
||||
data.filters.triggers.forEach((trigger) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = trigger;
|
||||
option.textContent = trigger;
|
||||
triggerSelectEl?.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
const initialTriggers = getQueryParamValues('trigger').filter((trigger) => data.filters.triggers.includes(trigger));
|
||||
const initialSort = getQueryParam('sort');
|
||||
|
||||
if (initialTriggers.length > 0) {
|
||||
currentFilters.triggers = initialTriggers;
|
||||
setSelectValues(triggerSelectEl, initialTriggers);
|
||||
}
|
||||
if (initialSort === 'lastUpdated') {
|
||||
currentSort = initialSort;
|
||||
if (sortSelect) sortSelect.value = initialSort;
|
||||
}
|
||||
|
||||
triggerSelectEl?.addEventListener('change', () => {
|
||||
currentFilters.triggers = getSelectValues(triggerSelectEl);
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as WorkflowSortOption;
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { triggers: [] };
|
||||
currentSort = 'title';
|
||||
clearSelectValues(triggerSelectEl);
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
syncUrlState();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
setupActionHandlers();
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initWorkflowsPage);
|
||||
@@ -945,436 +945,10 @@ body:has(#main-content) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000001;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal.hidden, .hidden {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.modal-header-top {
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.modal-header-top h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-emphasis);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-file-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.modal-file-switcher.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-file-switcher label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.modal-file-switcher-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.modal-file-dropdown {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: min(320px, 100%);
|
||||
max-width: min(420px, 100%);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-file-button {
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.modal-file-button span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-file-toggle {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding: 10px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.modal-file-toggle svg {
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.modal-file-dropdown.open .modal-file-toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.modal-file-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition: all var(--transition);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-file-dropdown.open .modal-file-menu {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-file-menu-item {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-file-menu-item:hover,
|
||||
.modal-file-menu-item:focus-visible {
|
||||
background: var(--color-bg-tertiary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-file-menu-item.active {
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
color: var(--color-text-emphasis);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-file-menu-item:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.modal-file-menu-item:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-actions button, .modal-actions div {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-body pre {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.modal.details-mode .modal-content {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.modal.details-mode #modal-file-switcher,
|
||||
.modal.details-mode #install-command-btn,
|
||||
.modal.details-mode #copy-btn,
|
||||
.modal.details-mode #download-btn,
|
||||
.modal.details-mode #share-btn,
|
||||
.modal.details-mode #render-btn,
|
||||
.modal.details-mode #raw-btn,
|
||||
.modal.details-mode #install-dropdown {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.modal.details-mode .modal-card-details {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.modal.details-mode .modal-card-details .resource-details-body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-rendered-content {
|
||||
padding: 24px;
|
||||
min-height: 200px;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-rendered-content > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-rendered-content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-code-content .shiki {
|
||||
margin: 0 !important;
|
||||
padding: 24px;
|
||||
min-height: 200px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Collection Modal View */
|
||||
.collection-view {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.collection-description {
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.collection-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.collection-items-header {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.collection-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.collection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.collection-item:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.collection-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.collection-item-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.collection-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.collection-item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collection-item-usage {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collection-item-type {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.collection-loading,
|
||||
.collection-error {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.collection-error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* External plugin badge */
|
||||
.resource-tag-external {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* External plugin modal metadata */
|
||||
.external-plugin-metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.external-plugin-meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.external-plugin-meta-label {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.external-plugin-meta-value {
|
||||
color: var(--color-text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.external-plugin-meta-value a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.external-plugin-meta-value a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.external-plugin-cta {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.external-plugin-repo-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.external-plugin-note {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Page Layouts */
|
||||
.page-header {
|
||||
padding: 40px 0 28px;
|
||||
@@ -3496,34 +3070,6 @@ header .theme-toggle-container {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MODAL ENTRANCE/EXIT ANIMATION
|
||||
============================================ */
|
||||
|
||||
.modal {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
transform: scale(0.95) translateY(10px);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal.visible .modal-content {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PAGE HEADER ENHANCEMENT
|
||||
============================================ */
|
||||
|
||||
Reference in New Issue
Block a user