diff --git a/website/src/pages/workflows.astro b/website/src/pages/workflows.astro
new file mode 100644
index 00000000..5f4074d2
--- /dev/null
+++ b/website/src/pages/workflows.astro
@@ -0,0 +1,54 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+import Modal from '../components/Modal.astro';
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/src/scripts/pages/index.ts b/website/src/scripts/pages/index.ts
index 82fbcd12..54dac8de 100644
--- a/website/src/scripts/pages/index.ts
+++ b/website/src/scripts/pages/index.ts
@@ -11,6 +11,7 @@ interface Manifest {
instructions: number;
skills: number;
hooks: number;
+ workflows: number;
plugins: number;
tools: number;
};
@@ -35,7 +36,7 @@ export async function initHomepage(): Promise
{
const manifest = await fetchData('manifest.json');
if (manifest && manifest.counts) {
// Populate counts in cards
- const countKeys = ['agents', 'instructions', 'skills', 'hooks', 'plugins', 'tools'] as const;
+ const countKeys = ['agents', 'instructions', 'skills', 'hooks', 'workflows', 'plugins', 'tools'] as const;
countKeys.forEach(key => {
const countEl = document.querySelector(`.card-count[data-count="${key}"]`);
if (countEl && manifest.counts[key] !== undefined) {
diff --git a/website/src/scripts/pages/workflows.ts b/website/src/scripts/pages/workflows.ts
new file mode 100644
index 00000000..ea76d569
--- /dev/null
+++ b/website/src/scripts/pages/workflows.ts
@@ -0,0 +1,235 @@
+/**
+ * Workflows page functionality
+ */
+import { createChoices, getChoicesValues, type Choices } from "../choices";
+import { FuzzySearch, type SearchItem } from "../search";
+import {
+ fetchData,
+ debounce,
+ escapeHtml,
+ getGitHubUrl,
+ getActionButtonsHtml,
+ setupActionHandlers,
+ getLastUpdatedHtml,
+} from "../utils";
+import { setupModal, openFileModal } from "../modal";
+
+interface Workflow extends SearchItem {
+ id: string;
+ path: string;
+ triggers: string[];
+ tags: string[];
+ lastUpdated?: string | null;
+}
+
+interface WorkflowsData {
+ items: Workflow[];
+ filters: {
+ triggers: string[];
+ tags: string[];
+ };
+}
+
+type SortOption = "title" | "lastUpdated";
+
+const resourceType = "workflow";
+let allItems: Workflow[] = [];
+let search = new FuzzySearch();
+let triggerSelect: Choices;
+let tagSelect: Choices;
+let currentFilters = {
+ triggers: [] as string[],
+ tags: [] as string[],
+};
+let currentSort: SortOption = "title";
+
+function sortItems(items: Workflow[]): Workflow[] {
+ return [...items].sort((a, b) => {
+ if (currentSort === "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);
+ });
+}
+
+function applyFiltersAndRender(): void {
+ const searchInput = document.getElementById(
+ "search-input"
+ ) as HTMLInputElement;
+ const countEl = document.getElementById("results-count");
+ const query = searchInput?.value || "";
+
+ let results = query ? search.search(query) : [...allItems];
+
+ if (currentFilters.triggers.length > 0) {
+ results = results.filter((item) =>
+ item.triggers.some((t) => currentFilters.triggers.includes(t))
+ );
+ }
+ if (currentFilters.tags.length > 0) {
+ results = results.filter((item) =>
+ item.tags.some((t) => currentFilters.tags.includes(t))
+ );
+ }
+
+ results = sortItems(results);
+
+ renderItems(results, query);
+ const activeFilters: string[] = [];
+ if (currentFilters.triggers.length > 0)
+ activeFilters.push(
+ `${currentFilters.triggers.length} trigger${
+ currentFilters.triggers.length > 1 ? "s" : ""
+ }`
+ );
+ if (currentFilters.tags.length > 0)
+ activeFilters.push(
+ `${currentFilters.tags.length} tag${
+ currentFilters.tags.length > 1 ? "s" : ""
+ }`
+ );
+ let countText = `${results.length} of ${allItems.length} workflows`;
+ if (activeFilters.length > 0) {
+ countText += ` (filtered by ${activeFilters.join(", ")})`;
+ }
+ if (countEl) countEl.textContent = countText;
+}
+
+function renderItems(items: Workflow[], query = ""): void {
+ const list = document.getElementById("resource-list");
+ if (!list) return;
+
+ if (items.length === 0) {
+ list.innerHTML =
+ 'No workflows found
Try a different search term or adjust filters
';
+ return;
+ }
+
+ list.innerHTML = items
+ .map(
+ (item) => `
+
+
+
${
+ query ? search.highlight(item.title, query) : escapeHtml(item.title)
+ }
+
${escapeHtml(
+ item.description || "No description"
+ )}
+
+ ${item.triggers
+ .map(
+ (t) =>
+ `${escapeHtml(t)}`
+ )
+ .join("")}
+ ${item.tags
+ .map(
+ (t) =>
+ `${escapeHtml(t)}`
+ )
+ .join("")}
+ ${getLastUpdatedHtml(item.lastUpdated)}
+
+
+
+ ${getActionButtonsHtml(item.path)}
+
GitHub
+
+
+ `
+ )
+ .join("");
+
+ // Add click handlers for opening modal
+ list.querySelectorAll(".resource-item").forEach((el) => {
+ el.addEventListener("click", (e) => {
+ if ((e.target as HTMLElement).closest(".resource-actions")) return;
+ const path = (el as HTMLElement).dataset.path;
+ if (path) openFileModal(path, resourceType);
+ });
+ });
+}
+
+export async function initWorkflowsPage(): Promise {
+ const list = document.getElementById("resource-list");
+ const searchInput = document.getElementById(
+ "search-input"
+ ) as HTMLInputElement;
+ const clearFiltersBtn = document.getElementById("clear-filters");
+ const sortSelect = document.getElementById(
+ "sort-select"
+ ) as HTMLSelectElement;
+
+ const data = await fetchData("workflows.json");
+ if (!data || !data.items) {
+ if (list)
+ list.innerHTML =
+ 'Failed to load data
';
+ return;
+ }
+
+ allItems = data.items;
+ search.setItems(allItems);
+
+ // Setup trigger filter
+ triggerSelect = createChoices("#filter-trigger", {
+ placeholderValue: "All Triggers",
+ });
+ triggerSelect.setChoices(
+ data.filters.triggers.map((t) => ({ value: t, label: t })),
+ "value",
+ "label",
+ true
+ );
+ document.getElementById("filter-trigger")?.addEventListener("change", () => {
+ currentFilters.triggers = getChoicesValues(triggerSelect);
+ applyFiltersAndRender();
+ });
+
+ // Setup tag filter
+ tagSelect = createChoices("#filter-tag", {
+ placeholderValue: "All Tags",
+ });
+ tagSelect.setChoices(
+ data.filters.tags.map((t) => ({ value: t, label: t })),
+ "value",
+ "label",
+ true
+ );
+ document.getElementById("filter-tag")?.addEventListener("change", () => {
+ currentFilters.tags = getChoicesValues(tagSelect);
+ applyFiltersAndRender();
+ });
+
+ sortSelect?.addEventListener("change", () => {
+ currentSort = sortSelect.value as SortOption;
+ applyFiltersAndRender();
+ });
+
+ applyFiltersAndRender();
+ searchInput?.addEventListener(
+ "input",
+ debounce(() => applyFiltersAndRender(), 200)
+ );
+
+ clearFiltersBtn?.addEventListener("click", () => {
+ currentFilters = { triggers: [], tags: [] };
+ currentSort = "title";
+ triggerSelect.removeActiveItems();
+ tagSelect.removeActiveItems();
+ if (searchInput) searchInput.value = "";
+ if (sortSelect) sortSelect.value = "title";
+ applyFiltersAndRender();
+ });
+
+ setupModal();
+ setupActionHandlers();
+}
+
+// Auto-initialize when DOM is ready
+document.addEventListener("DOMContentLoaded", initWorkflowsPage);
diff --git a/website/src/scripts/utils.ts b/website/src/scripts/utils.ts
index ee6fe2a9..6ea97853 100644
--- a/website/src/scripts/utils.ts
+++ b/website/src/scripts/utils.ts
@@ -228,6 +228,8 @@ export function getResourceType(filePath: string): string {
return "skill";
if (/(^|\/)hooks\//.test(filePath) && filePath.endsWith("README.md"))
return "hook";
+ if (/(^|\/)workflows\//.test(filePath) && filePath.endsWith(".md"))
+ return "workflow";
// Check for plugin directories (e.g., plugins/, plugins//)
if (/(^|\/)plugins\/[^/]+\/?$/.test(filePath)) return "plugin";
// Check for plugin.json files (e.g., plugins//.github/plugin/plugin.json)
@@ -244,6 +246,7 @@ export function formatResourceType(type: string): string {
instruction: "📋 Instruction",
skill: "⚡ Skill",
hook: "🪝 Hook",
+ workflow: "⚡ Workflow",
plugin: "🔌 Plugin",
};
return labels[type] || type;
@@ -258,6 +261,7 @@ export function getResourceIcon(type: string): string {
instruction: "📋",
skill: "⚡",
hook: "🪝",
+ workflow: "⚡",
plugin: "🔌",
};
return icons[type] || "📄";