chore: publish from main

This commit is contained in:
github-actions[bot]
2026-07-16 04:10:37 +00:00
parent d86f40c898
commit d925e492ac
22 changed files with 64 additions and 1949 deletions
-3
View File
@@ -129,11 +129,8 @@ export default defineConfig({
{ label: "Agents", link: "/agents/" },
{ label: "Instructions", link: "/instructions/" },
{ label: "Skills", link: "/skills/" },
{ label: "Hooks", link: "/hooks/" },
{ label: "Workflows", link: "/workflows/" },
{ label: "Canvas Extensions", link: "/extensions/" },
{ label: "Plugins", link: "/plugins/" },
{ label: "Tools", link: "/tools/" },
{ label: "Contributors", link: "/contributors/" },
],
},
@@ -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
@@ -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
---
-159
View File
@@ -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>
-56
View File
@@ -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 &amp; 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>
-55
View File
@@ -116,44 +116,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 +158,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"
-277
View File
@@ -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 &amp; 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>
-131
View File
@@ -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>
-56
View File
@@ -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 &amp; 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>
-104
View File
@@ -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("");
}
-199
View File
@@ -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);
-203
View File
@@ -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("");
}
-211
View File
@@ -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('');
}
-139
View File
@@ -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);