mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-30 12:15:56 +00:00
Simplify website search and listing controls (#1553)
* Removing search from the home pageThis was a little confusing because there are two searches, but the overall site search is a lot more powerful * Prefilter website search by resource page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * small error handling and formatting * Simplify website listing controls Remove per-page text search, trim page-specific controls, and move remaining sort/filter controls into compact flyouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -354,50 +354,6 @@ function generateInstructionsData(gitDates) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Categorize a skill based on its name and description
|
|
||||||
*/
|
|
||||||
function categorizeSkill(name, description) {
|
|
||||||
const text = `${name} ${description}`.toLowerCase();
|
|
||||||
|
|
||||||
if (text.includes("azure") || text.includes("appinsights")) return "Azure";
|
|
||||||
if (
|
|
||||||
text.includes("github") ||
|
|
||||||
text.includes("gh-cli") ||
|
|
||||||
text.includes("git-commit") ||
|
|
||||||
text.includes("git ")
|
|
||||||
)
|
|
||||||
return "Git & GitHub";
|
|
||||||
if (text.includes("vscode") || text.includes("vs code")) return "VS Code";
|
|
||||||
if (
|
|
||||||
text.includes("test") ||
|
|
||||||
text.includes("qa") ||
|
|
||||||
text.includes("playwright")
|
|
||||||
)
|
|
||||||
return "Testing";
|
|
||||||
if (
|
|
||||||
text.includes("microsoft") ||
|
|
||||||
text.includes("m365") ||
|
|
||||||
text.includes("workiq")
|
|
||||||
)
|
|
||||||
return "Microsoft";
|
|
||||||
if (text.includes("cli") || text.includes("command")) return "CLI Tools";
|
|
||||||
if (
|
|
||||||
text.includes("diagram") ||
|
|
||||||
text.includes("plantuml") ||
|
|
||||||
text.includes("visual")
|
|
||||||
)
|
|
||||||
return "Diagrams";
|
|
||||||
if (
|
|
||||||
text.includes("nuget") ||
|
|
||||||
text.includes("dotnet") ||
|
|
||||||
text.includes(".net")
|
|
||||||
)
|
|
||||||
return ".NET";
|
|
||||||
|
|
||||||
return "Other";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate skills metadata
|
* Generate skills metadata
|
||||||
*/
|
*/
|
||||||
@@ -405,15 +361,13 @@ function generateSkillsData(gitDates) {
|
|||||||
const skills = [];
|
const skills = [];
|
||||||
|
|
||||||
if (!fs.existsSync(SKILLS_DIR)) {
|
if (!fs.existsSync(SKILLS_DIR)) {
|
||||||
return { items: [], filters: { categories: [], hasAssets: ["Yes", "No"] } };
|
return { items: [], filters: { hasAssets: ["Yes", "No"] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = fs
|
const folders = fs
|
||||||
.readdirSync(SKILLS_DIR)
|
.readdirSync(SKILLS_DIR)
|
||||||
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
|
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
|
||||||
|
|
||||||
const allCategories = new Set();
|
|
||||||
|
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
const skillPath = path.join(SKILLS_DIR, folder);
|
const skillPath = path.join(SKILLS_DIR, folder);
|
||||||
const metadata = parseSkillMetadata(skillPath);
|
const metadata = parseSkillMetadata(skillPath);
|
||||||
@@ -422,8 +376,6 @@ function generateSkillsData(gitDates) {
|
|||||||
const relativePath = path
|
const relativePath = path
|
||||||
.relative(ROOT_FOLDER, skillPath)
|
.relative(ROOT_FOLDER, skillPath)
|
||||||
.replace(/\\/g, "/");
|
.replace(/\\/g, "/");
|
||||||
const category = categorizeSkill(metadata.name, metadata.description);
|
|
||||||
allCategories.add(category);
|
|
||||||
|
|
||||||
// Get all files in the skill folder recursively
|
// Get all files in the skill folder recursively
|
||||||
const files = getSkillFiles(skillPath, relativePath);
|
const files = getSkillFiles(skillPath, relativePath);
|
||||||
@@ -440,7 +392,6 @@ function generateSkillsData(gitDates) {
|
|||||||
folder,
|
folder,
|
||||||
metadata.name,
|
metadata.name,
|
||||||
relativePath,
|
relativePath,
|
||||||
category,
|
|
||||||
]
|
]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
@@ -453,7 +404,6 @@ function generateSkillsData(gitDates) {
|
|||||||
assets: metadata.assets,
|
assets: metadata.assets,
|
||||||
hasAssets: metadata.assets.length > 0,
|
hasAssets: metadata.assets.length > 0,
|
||||||
assetCount: metadata.assets.length,
|
assetCount: metadata.assets.length,
|
||||||
category: category,
|
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
skillFile: skillFilePath,
|
skillFile: skillFilePath,
|
||||||
files: files,
|
files: files,
|
||||||
@@ -468,7 +418,6 @@ function generateSkillsData(gitDates) {
|
|||||||
return {
|
return {
|
||||||
items: sortedSkills,
|
items: sortedSkills,
|
||||||
filters: {
|
filters: {
|
||||||
categories: Array.from(allCategories).sort(),
|
|
||||||
hasAssets: ["Yes", "No"],
|
hasAssets: ["Yes", "No"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -976,9 +925,7 @@ async function main() {
|
|||||||
|
|
||||||
const skillsData = generateSkillsData(gitDates);
|
const skillsData = generateSkillsData(gitDates);
|
||||||
const skills = skillsData.items;
|
const skills = skillsData.items;
|
||||||
console.log(
|
console.log(`✓ Generated ${skills.length} skills`);
|
||||||
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
|
|
||||||
);
|
|
||||||
|
|
||||||
const pluginsData = generatePluginsData(gitDates);
|
const pluginsData = generatePluginsData(gitDates);
|
||||||
const plugins = pluginsData.items;
|
const plugins = pluginsData.items;
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export default defineConfig({
|
|||||||
components: {
|
components: {
|
||||||
Head: "./src/components/Head.astro",
|
Head: "./src/components/Head.astro",
|
||||||
Footer: "./src/components/Footer.astro",
|
Footer: "./src/components/Footer.astro",
|
||||||
|
Search: "./src/components/Search.astro",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
sitemap(),
|
sitemap(),
|
||||||
|
|||||||
516
website/src/components/Search.astro
Normal file
516
website/src/components/Search.astro
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
---
|
||||||
|
import Icon from './Icon.astro';
|
||||||
|
import project from 'virtual:starlight/project-context';
|
||||||
|
|
||||||
|
const pagefindTranslations = {
|
||||||
|
placeholder: Astro.locals.t('search.label'),
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(Astro.locals.t.all())
|
||||||
|
.filter(([key]) => key.startsWith('pagefind.'))
|
||||||
|
.map(([key, value]) => [key.replace('pagefind.', ''), value])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataAttributes: DOMStringMap = { 'data-translations': JSON.stringify(pagefindTranslations) };
|
||||||
|
if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash'] = '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<site-search class={Astro.props.class} {...dataAttributes}>
|
||||||
|
<button
|
||||||
|
data-open-modal
|
||||||
|
disabled
|
||||||
|
aria-label={Astro.locals.t('search.label')}
|
||||||
|
aria-keyshortcuts="Control+K"
|
||||||
|
>
|
||||||
|
<Icon name="search" />
|
||||||
|
<span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
|
||||||
|
<kbd class="sl-hidden md:sl-flex" style="display: none;">
|
||||||
|
<kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog style="padding:0" aria-label={Astro.locals.t('search.label')}>
|
||||||
|
<div class="dialog-frame sl-flex">
|
||||||
|
{
|
||||||
|
/* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
|
||||||
|
}
|
||||||
|
<button data-close-modal class="sl-flex md:sl-hidden">
|
||||||
|
{Astro.locals.t('search.cancelLabel')}
|
||||||
|
</button>
|
||||||
|
{
|
||||||
|
import.meta.env.DEV ? (
|
||||||
|
<div style="margin: auto; text-align: center; white-space: pre-line;" dir="ltr">
|
||||||
|
<p>{Astro.locals.t('search.devWarning')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="search-container">
|
||||||
|
<div id="starlight__search" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</site-search>
|
||||||
|
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This is intentionally inlined to avoid briefly showing an invalid shortcut.
|
||||||
|
* Purposely using the deprecated `navigator.platform` property to detect Apple devices, as the
|
||||||
|
* user agent is spoofed by some browsers when opening the devtools.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
<script is:inline>
|
||||||
|
(() => {
|
||||||
|
const openBtn = document.querySelector('button[data-open-modal]');
|
||||||
|
const shortcut = openBtn?.querySelector('kbd');
|
||||||
|
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
|
||||||
|
const platformKey = shortcut.querySelector('kbd');
|
||||||
|
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
|
||||||
|
platformKey.textContent = '⌘';
|
||||||
|
openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
|
||||||
|
}
|
||||||
|
shortcut.style.display = '';
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { pagefindUserConfig } from 'virtual:starlight/pagefind-config';
|
||||||
|
|
||||||
|
const ROUTE_TYPE_FILTERS: Record<string, string> = {
|
||||||
|
'/agents': 'agent',
|
||||||
|
'/instructions': 'instruction',
|
||||||
|
'/skills': 'skill',
|
||||||
|
'/hooks': 'hook',
|
||||||
|
'/workflows': 'workflow',
|
||||||
|
'/plugins': 'plugin',
|
||||||
|
'/tools': 'tool',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRouteTypeFilter(pathname: string, baseUrl: string): string | undefined {
|
||||||
|
const normalizedBaseUrl = baseUrl === '/' ? '' : baseUrl.replace(/\/$/, '');
|
||||||
|
const pathWithoutBase =
|
||||||
|
normalizedBaseUrl && pathname.startsWith(normalizedBaseUrl)
|
||||||
|
? pathname.slice(normalizedBaseUrl.length) || '/'
|
||||||
|
: pathname;
|
||||||
|
const normalizedPath = pathWithoutBase.replace(/\/$/, '') || '/';
|
||||||
|
return ROUTE_TYPE_FILTERS[normalizedPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SiteSearch extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const openBtn = this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
|
||||||
|
const closeBtn = this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
|
||||||
|
const dialog = this.querySelector('dialog')!;
|
||||||
|
const dialogFrame = this.querySelector('.dialog-frame')!;
|
||||||
|
|
||||||
|
/** Close the modal if a user clicks on a link or outside of the modal. */
|
||||||
|
const onClick = (event: MouseEvent) => {
|
||||||
|
const isLink = 'href' in (event.target || {});
|
||||||
|
if (
|
||||||
|
isLink ||
|
||||||
|
(document.body.contains(event.target as Node) &&
|
||||||
|
!dialogFrame.contains(event.target as Node))
|
||||||
|
) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = (event?: MouseEvent) => {
|
||||||
|
dialog.showModal();
|
||||||
|
document.body.toggleAttribute('data-search-modal-open', true);
|
||||||
|
this.querySelector('input')?.focus();
|
||||||
|
event?.stopPropagation();
|
||||||
|
window.addEventListener('click', onClick);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => dialog.close();
|
||||||
|
|
||||||
|
openBtn.addEventListener('click', openModal);
|
||||||
|
openBtn.disabled = false;
|
||||||
|
closeBtn.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
dialog.addEventListener('close', () => {
|
||||||
|
document.body.toggleAttribute('data-search-modal-open', false);
|
||||||
|
window.removeEventListener('click', onClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for `ctrl + k` and `cmd + k` keyboard shortcuts.
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'k') {
|
||||||
|
dialog.open ? closeModal() : openModal();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let translations = {};
|
||||||
|
try {
|
||||||
|
translations = JSON.parse(this.dataset.translations || '{}');
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const shouldStrip = this.dataset.stripTrailingSlash !== undefined;
|
||||||
|
const stripTrailingSlash = (path: string) => path.replace(/(.)\/(#.*)?$/, '$1$2');
|
||||||
|
const formatURL = shouldStrip ? stripTrailingSlash : (path: string) => path;
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (import.meta.env.DEV) return;
|
||||||
|
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
|
||||||
|
onIdle(async () => {
|
||||||
|
// @ts-expect-error — Missing types for @pagefind/default-ui package.
|
||||||
|
const { PagefindUI } = await import('@pagefind/default-ui');
|
||||||
|
const pagefind = new PagefindUI({
|
||||||
|
...pagefindUserConfig,
|
||||||
|
element: '#starlight__search',
|
||||||
|
baseUrl: import.meta.env.BASE_URL,
|
||||||
|
bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind/',
|
||||||
|
showImages: false,
|
||||||
|
translations,
|
||||||
|
showSubResults: true,
|
||||||
|
processResult: (result: { url: string; sub_results: Array<{ url: string }> }) => {
|
||||||
|
result.url = formatURL(result.url);
|
||||||
|
result.sub_results = result.sub_results.map((sub_result) => {
|
||||||
|
sub_result.url = formatURL(sub_result.url);
|
||||||
|
return sub_result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeTypeFilter = getRouteTypeFilter(
|
||||||
|
window.location.pathname,
|
||||||
|
import.meta.env.BASE_URL
|
||||||
|
);
|
||||||
|
if (routeTypeFilter) {
|
||||||
|
pagefind.triggerFilters({ type: routeTypeFilter });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('site-search', SiteSearch);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@layer starlight.core {
|
||||||
|
site-search {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
button[data-open-modal] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--sl-color-gray-1);
|
||||||
|
cursor: pointer;
|
||||||
|
height: 2.5rem;
|
||||||
|
font-size: var(--sl-text-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
button[data-open-modal] {
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding-inline-start: 0.75rem;
|
||||||
|
padding-inline-end: 0.5rem;
|
||||||
|
background-color: var(--sl-color-black);
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
font-size: var(--sl-text-sm);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
button[data-open-modal]:hover {
|
||||||
|
border-color: var(--sl-color-gray-2);
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
button[data-open-modal] > :last-child {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button > kbd {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: var(--sl-text-2xs);
|
||||||
|
gap: 0.25em;
|
||||||
|
padding-inline: 0.375rem;
|
||||||
|
background-color: var(--sl-color-gray-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-family: var(--__sl-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--sl-color-gray-6);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
box-shadow: var(--sl-shadow-lg);
|
||||||
|
}
|
||||||
|
dialog[open] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background-color: var(--sl-color-backdrop-overlay);
|
||||||
|
-webkit-backdrop-filter: blur(0.25rem);
|
||||||
|
backdrop-filter: blur(0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-frame {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[data-close-modal] {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
align-items: center;
|
||||||
|
align-self: flex-end;
|
||||||
|
height: calc(64px * var(--pagefind-ui-scale));
|
||||||
|
padding: 0.25rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--sl-color-text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search {
|
||||||
|
--pagefind-ui-primary: var(--sl-color-text);
|
||||||
|
--pagefind-ui-text: var(--sl-color-gray-2);
|
||||||
|
--pagefind-ui-font: var(--__sl-font);
|
||||||
|
--pagefind-ui-background: var(--sl-color-black);
|
||||||
|
--pagefind-ui-border: var(--sl-color-gray-5);
|
||||||
|
--pagefind-ui-border-width: 1px;
|
||||||
|
--pagefind-ui-tag: var(--sl-color-gray-5);
|
||||||
|
--sl-search-cancel-space: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='light'] #starlight__search {
|
||||||
|
--pagefind-ui-tag: var(--sl-color-gray-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
#starlight__search {
|
||||||
|
--sl-search-cancel-space: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
margin: 4rem auto auto;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 40rem;
|
||||||
|
height: max-content;
|
||||||
|
min-height: 15rem;
|
||||||
|
max-height: calc(100% - 8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-frame {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
@import url('@pagefind/default-ui/css/ui.css') layer(starlight.core);
|
||||||
|
|
||||||
|
@layer starlight.core {
|
||||||
|
[data-search-modal-open] {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search {
|
||||||
|
--sl-search-result-spacing: calc(1.25rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-result-pad-inline-start: calc(3.75rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-result-pad-inline-end: calc(1.25rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-result-pad-block: calc(0.9375rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-result-nested-pad-block: calc(0.625rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-corners: calc(0.3125rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-page-icon-size: calc(1.875rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-page-icon-inline-start: calc(
|
||||||
|
(var(--sl-search-result-pad-inline-start) - var(--sl-search-page-icon-size)) / 2
|
||||||
|
);
|
||||||
|
--sl-search-tree-diagram-size: calc(2.5rem * var(--pagefind-ui-scale));
|
||||||
|
--sl-search-tree-diagram-inline-start: calc(
|
||||||
|
(var(--sl-search-result-pad-inline-start) - var(--sl-search-tree-diagram-size)) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__form::before {
|
||||||
|
--pagefind-ui-text: var(--sl-color-gray-1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__search-input {
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font-weight: 400;
|
||||||
|
width: calc(100% - var(--sl-search-cancel-space));
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search input:focus {
|
||||||
|
--pagefind-ui-border: var(--sl-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__search-clear {
|
||||||
|
inset-inline-end: var(--sl-search-cancel-space);
|
||||||
|
width: calc(60px * var(--pagefind-ui-scale));
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__search-clear:focus {
|
||||||
|
outline: 1px solid var(--sl-color-accent);
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__search-clear::before {
|
||||||
|
content: '';
|
||||||
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
|
||||||
|
center / 50% no-repeat;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
|
||||||
|
center / 50% no-repeat;
|
||||||
|
background-color: var(--sl-color-text-accent);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__results > * + * {
|
||||||
|
margin-top: var(--sl-search-result-spacing);
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__result {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-nested {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--sl-search-result-nested-pad-block) var(--sl-search-result-pad-inline-end);
|
||||||
|
padding-inline-start: var(--sl-search-result-pad-inline-start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)),
|
||||||
|
#starlight__search .pagefind-ui__result-nested {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--sl-color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):hover,
|
||||||
|
#starlight__search
|
||||||
|
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,
|
||||||
|
#starlight__search .pagefind-ui__result-nested:hover,
|
||||||
|
#starlight__search .pagefind-ui__result-nested:focus-within {
|
||||||
|
outline: 1px solid var(--sl-color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search
|
||||||
|
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):focus-within,
|
||||||
|
#starlight__search .pagefind-ui__result-nested:focus-within {
|
||||||
|
background-color: var(--sl-color-accent-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-thumb,
|
||||||
|
#starlight__search .pagefind-ui__result-inner {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-inner > :first-child {
|
||||||
|
border-radius: var(--sl-search-corners) var(--sl-search-corners) 0 0;
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__result-inner > :last-child {
|
||||||
|
border-radius: 0 0 var(--sl-search-corners) var(--sl-search-corners);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-inner > .pagefind-ui__result-title {
|
||||||
|
padding: var(--sl-search-result-pad-block) var(--sl-search-result-pad-inline-end);
|
||||||
|
padding-inline-start: var(--sl-search-result-pad-inline-start);
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__result-inner > .pagefind-ui__result-title::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 0;
|
||||||
|
inset-inline-start: var(--sl-search-page-icon-inline-start);
|
||||||
|
width: var(--sl-search-page-icon-size);
|
||||||
|
background: var(--sl-color-gray-3);
|
||||||
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
|
||||||
|
center no-repeat;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
|
||||||
|
center no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-inner {
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-link {
|
||||||
|
position: unset;
|
||||||
|
--pagefind-ui-text: var(--sl-color-white);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-link:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-nested .pagefind-ui__result-link::before {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-nested::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 0;
|
||||||
|
inset-inline-start: var(--sl-search-tree-diagram-inline-start);
|
||||||
|
width: var(--sl-search-tree-diagram-size);
|
||||||
|
background: var(--sl-color-gray-4);
|
||||||
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
|
||||||
|
0% 0% / 100% no-repeat;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
|
||||||
|
0% 0% / 100% no-repeat;
|
||||||
|
}
|
||||||
|
#starlight__search .pagefind-ui__result-nested:last-of-type::before {
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flip page and tree icons around the vertical axis when in an RTL layout. */
|
||||||
|
[dir='rtl'] .pagefind-ui__result-title::before,
|
||||||
|
[dir='rtl'] .pagefind-ui__result-nested::before {
|
||||||
|
transform: matrix(-1, 0, 0, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-excerpt {
|
||||||
|
font-size: calc(1rem * var(--pagefind-ui-scale));
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search mark {
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
background-color: transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__filter-value::before {
|
||||||
|
border-color: var(--sl-color-text-invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
#starlight__search .pagefind-ui__result-tags {
|
||||||
|
background-color: var(--sl-color-black);
|
||||||
|
margin-top: 0;
|
||||||
|
padding: var(--sl-search-result-nested-pad-block) var(--sl-search-result-pad-inline-end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -64,6 +64,10 @@ export default function pagefindResources(): AstroIntegration {
|
|||||||
}
|
}
|
||||||
const { index } = response;
|
const { index } = response;
|
||||||
|
|
||||||
|
if (!index) {
|
||||||
|
throw new Error("Pagefind index is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
// Index all built HTML pages (same as Starlight's default)
|
// Index all built HTML pages (same as Starlight's default)
|
||||||
const indexResult = await index.addDirectory({
|
const indexResult = await index.addDirectory({
|
||||||
path: fileURLToPath(dir),
|
path: fileURLToPath(dir),
|
||||||
@@ -82,7 +86,9 @@ export default function pagefindResources(): AstroIntegration {
|
|||||||
try {
|
try {
|
||||||
records = JSON.parse(readFileSync(searchIndexPath, "utf-8"));
|
records = JSON.parse(readFileSync(searchIndexPath, "utf-8"));
|
||||||
} catch {
|
} catch {
|
||||||
log.warn("Could not read search-index.json, skipping resource indexing.");
|
log.warn(
|
||||||
|
"Could not read search-index.json, skipping resource indexing."
|
||||||
|
);
|
||||||
records = [];
|
records = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +100,15 @@ export default function pagefindResources(): AstroIntegration {
|
|||||||
const typePage = TYPE_PAGES[record.type];
|
const typePage = TYPE_PAGES[record.type];
|
||||||
if (!typePage) continue;
|
if (!typePage) continue;
|
||||||
|
|
||||||
const url = `${base}${typePage.slice(1)}#file=${encodeURIComponent(record.path)}`;
|
const url = `${base}${typePage.slice(1)}#file=${encodeURIComponent(
|
||||||
|
record.path
|
||||||
|
)}`;
|
||||||
const typeLabel = TYPE_LABELS[record.type] || record.type;
|
const typeLabel = TYPE_LABELS[record.type] || record.type;
|
||||||
|
|
||||||
const addResult = await index.addCustomRecord({
|
const addResult = await index.addCustomRecord({
|
||||||
url,
|
url,
|
||||||
content: record.searchText || `${record.title} ${record.description}`,
|
content:
|
||||||
|
record.searchText || `${record.title} ${record.description}`,
|
||||||
language: "en",
|
language: "en",
|
||||||
meta: {
|
meta: {
|
||||||
title: `${record.title} — ${typeLabel}`,
|
title: `${record.title} — ${typeLabel}`,
|
||||||
@@ -110,7 +119,8 @@ export default function pagefindResources(): AstroIntegration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (addResult.errors.length > 0) {
|
if (addResult.errors.length > 0) {
|
||||||
for (const err of addResult.errors) log.warn(`Record ${record.id}: ${err}`);
|
for (const err of addResult.errors)
|
||||||
|
log.warn(`Record ${record.id}: ${err}`);
|
||||||
} else {
|
} else {
|
||||||
added++;
|
added++;
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,11 @@ export default function pagefindResources(): AstroIntegration {
|
|||||||
|
|
||||||
const elapsed = performance.now() - now;
|
const elapsed = performance.now() - now;
|
||||||
log.info(
|
log.info(
|
||||||
`Search index built in ${elapsed < 750 ? `${Math.round(elapsed)}ms` : `${(elapsed / 1000).toFixed(2)}s`}.`
|
`Search index built in ${
|
||||||
|
elapsed < 750
|
||||||
|
? `${Math.round(elapsed)}ms`
|
||||||
|
: `${(elapsed / 1000).toFixed(2)}s`
|
||||||
|
}.`
|
||||||
);
|
);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
throw new Error("Failed to build Pagefind search index.", { cause });
|
throw new Error("Failed to build Pagefind search index.", { cause });
|
||||||
|
|||||||
@@ -18,27 +18,12 @@ const initialItems = sortAgents(agentsData.items, 'title');
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search agents</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} agents</div>
|
||||||
<input type="text" id="search-input" placeholder="Search agents..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort</summary>
|
||||||
|
<div class="listing-controls-panel">
|
||||||
<!-- Filters -->
|
|
||||||
<div class="filters-bar" id="filters-bar">
|
<div class="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
|
||||||
<label for="filter-model">Model:</label>
|
|
||||||
<select id="filter-model" multiple aria-label="Filter by model"></select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label for="filter-tool">Tool:</label>
|
|
||||||
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="filter-handoffs">
|
|
||||||
Has Handoffs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="sort-select">Sort:</label>
|
<label for="sort-select">Sort:</label>
|
||||||
<select id="sort-select" aria-label="Sort by">
|
<select id="sort-select" aria-label="Sort by">
|
||||||
@@ -46,11 +31,11 @@ const initialItems = sortAgents(agentsData.items, 'title');
|
|||||||
<option value="lastUpdated">Recently Updated</option>
|
<option value="lastUpdated">Recently Updated</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} agents</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderAgentsHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderAgentsHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="agents" />
|
<ContributeCTA resourceType="agents" />
|
||||||
</div>
|
</div>
|
||||||
@@ -62,6 +47,7 @@ const initialItems = sortAgents(agentsData.items, 'title');
|
|||||||
<EmbeddedPageData filename="agents.json" data={agentsData} />
|
<EmbeddedPageData filename="agents.json" data={agentsData} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/agents';
|
import '../scripts/pages/agents';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
@@ -18,16 +18,12 @@ const initialItems = sortHooks(hooksData.items, 'title');
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search hooks</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} hooks</div>
|
||||||
<input type="text" id="search-input" placeholder="Search hooks..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<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="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
|
||||||
<label for="filter-hook">Hook Event:</label>
|
|
||||||
<select id="filter-hook" multiple aria-label="Filter by hook event"></select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="filter-tag">Tag:</label>
|
<label for="filter-tag">Tag:</label>
|
||||||
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
||||||
@@ -42,8 +38,9 @@ const initialItems = sortHooks(hooksData.items, 'title');
|
|||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} hooks</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="hooks" />
|
<ContributeCTA resourceType="hooks" />
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +52,7 @@ const initialItems = sortHooks(hooksData.items, 'title');
|
|||||||
|
|
||||||
<EmbeddedPageData filename="hooks.json" data={hooksData} />
|
<EmbeddedPageData filename="hooks.json" data={hooksData} />
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/hooks';
|
import '../scripts/pages/hooks';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
@@ -40,49 +40,6 @@ const base = import.meta.env.BASE_URL;
|
|||||||
Community-contributed agents, instructions, and skills to enhance your
|
Community-contributed agents, instructions, and skills to enhance your
|
||||||
GitHub Copilot experience
|
GitHub Copilot experience
|
||||||
</p>
|
</p>
|
||||||
<div class="hero-search">
|
|
||||||
<label for="global-search" class="sr-only">Search all resources</label
|
|
||||||
>
|
|
||||||
<p id="global-search-help" class="sr-only">
|
|
||||||
Type at least two characters to show matching resources, then press
|
|
||||||
the Down Arrow key to move into the results.
|
|
||||||
</p>
|
|
||||||
<p id="global-search-status" class="sr-only" aria-live="polite"></p>
|
|
||||||
<div class="search-row">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="global-search"
|
|
||||||
placeholder="Search all resources..."
|
|
||||||
autocomplete="off"
|
|
||||||
aria-describedby="global-search-help global-search-status"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="https://github.com/github/awesome-copilot"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="github-btn"
|
|
||||||
aria-label="View on GitHub"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<span>GitHub</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="search-results"
|
|
||||||
class="search-results hidden"
|
|
||||||
aria-label="Search results"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search instructions</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} instructions</div>
|
||||||
<input type="text" id="search-input" placeholder="Search instructions..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<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="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="filter-extension">File Extension:</label>
|
<label for="filter-extension">File Extension:</label>
|
||||||
@@ -38,8 +38,9 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
|
|||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} instructions</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderInstructionsHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderInstructionsHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="instructions" />
|
<ContributeCTA resourceType="instructions" />
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +52,7 @@ const initialItems = sortInstructions(instructionsData.items, 'title');
|
|||||||
<EmbeddedPageData filename="instructions.json" data={instructionsData} />
|
<EmbeddedPageData filename="instructions.json" data={instructionsData} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/instructions';
|
import '../scripts/pages/instructions';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import ContributeCTA from '../components/ContributeCTA.astro';
|
|||||||
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
|
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
|
||||||
import PageHeader from '../components/PageHeader.astro';
|
import PageHeader from '../components/PageHeader.astro';
|
||||||
import BackToTop from '../components/BackToTop.astro';
|
import BackToTop from '../components/BackToTop.astro';
|
||||||
import { renderPluginsHtml } from '../scripts/pages/plugins-render';
|
import { renderPluginsHtml, sortPlugins } from '../scripts/pages/plugins-render';
|
||||||
|
|
||||||
const initialItems = pluginsData.items;
|
const initialItems = sortPlugins(pluginsData.items, 'title');
|
||||||
---
|
---
|
||||||
|
|
||||||
<StarlightPage frontmatter={{ title: 'Plugins', description: 'Curated plugins of agents, hooks, and skills for specific workflows', template: 'splash', prev: false, next: false, editUrl: false }}>
|
<StarlightPage frontmatter={{ title: 'Plugins', description: 'Curated plugins of agents, hooks, and skills for specific workflows', template: 'splash', prev: false, next: false, editUrl: false }}>
|
||||||
@@ -27,21 +27,29 @@ const initialItems = pluginsData.items;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search plugins</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} plugins</div>
|
||||||
<input type="text" id="search-input" placeholder="Search plugins..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<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="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="filter-tag">Tag:</label>
|
<label for="filter-tag">Tag:</label>
|
||||||
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="sort-select">Sort:</label>
|
||||||
|
<select id="sort-select" aria-label="Sort plugins">
|
||||||
|
<option value="title">Title</option>
|
||||||
|
<option value="lastUpdated">Recently Updated</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} plugins</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderPluginsHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderPluginsHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="plugins" />
|
<ContributeCTA resourceType="plugins" />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +61,7 @@ const initialItems = pluginsData.items;
|
|||||||
<EmbeddedPageData filename="plugins.json" data={pluginsData} />
|
<EmbeddedPageData filename="plugins.json" data={pluginsData} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/plugins';
|
import '../scripts/pages/plugins';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -18,22 +18,12 @@ const initialItems = sortSkills(skillsData.items, 'title');
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search skills</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} skills</div>
|
||||||
<input type="text" id="search-input" placeholder="Search skills..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort</summary>
|
||||||
|
<div class="listing-controls-panel">
|
||||||
<div class="filters-bar" id="filters-bar">
|
<div class="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
|
||||||
<label for="filter-category">Category:</label>
|
|
||||||
<select id="filter-category" multiple aria-label="Filter by category"></select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" id="filter-has-assets">
|
|
||||||
Has Bundled Assets
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="sort-select">Sort:</label>
|
<label for="sort-select">Sort:</label>
|
||||||
<select id="sort-select" aria-label="Sort by">
|
<select id="sort-select" aria-label="Sort by">
|
||||||
@@ -41,11 +31,11 @@ const initialItems = sortSkills(skillsData.items, 'title');
|
|||||||
<option value="lastUpdated">Recently Updated</option>
|
<option value="lastUpdated">Recently Updated</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} skills</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderSkillsHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderSkillsHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="skills" />
|
<ContributeCTA resourceType="skills" />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +47,7 @@ const initialItems = sortSkills(skillsData.items, 'title');
|
|||||||
|
|
||||||
<EmbeddedPageData filename="skills.json" data={skillsData} />
|
<EmbeddedPageData filename="skills.json" data={skillsData} />
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/skills';
|
import '../scripts/pages/skills';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import ContributeCTA from "../components/ContributeCTA.astro";
|
|||||||
import EmbeddedPageData from "../components/EmbeddedPageData.astro";
|
import EmbeddedPageData from "../components/EmbeddedPageData.astro";
|
||||||
import PageHeader from "../components/PageHeader.astro";
|
import PageHeader from "../components/PageHeader.astro";
|
||||||
import BackToTop from '../components/BackToTop.astro';
|
import BackToTop from '../components/BackToTop.astro';
|
||||||
import { renderToolsHtml } from "../scripts/pages/tools-render";
|
import { renderToolsHtml, sortTools } from "../scripts/pages/tools-render";
|
||||||
|
|
||||||
const initialItems = toolsData.items.map((item) => ({
|
const initialItems = sortTools(
|
||||||
|
toolsData.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
title: item.name,
|
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 }}>
|
<StarlightPage frontmatter={{ title: 'Tools', description: 'MCP servers and developer tools for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
|
||||||
@@ -20,29 +23,34 @@ const initialItems = toolsData.items.map((item) => ({
|
|||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-section">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search tools</label>
|
<div id="results-count" class="results-count" aria-live="polite">{initialItems.length} tools</div>
|
||||||
<input
|
<details class="listing-controls">
|
||||||
type="text"
|
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort & Filter</summary>
|
||||||
id="search-input"
|
<div class="listing-controls-panel">
|
||||||
placeholder="Search tools..."
|
<div class="filters-bar" id="filters-bar">
|
||||||
class="search-input"
|
<div class="filter-group">
|
||||||
/>
|
<label for="filter-category">Category:</label>
|
||||||
</div>
|
|
||||||
<div class="filters">
|
|
||||||
<label for="filter-category" class="sr-only">Filter by category</label>
|
|
||||||
<select id="filter-category" class="filter-select" aria-label="Filter by category">
|
<select id="filter-category" class="filter-select" aria-label="Filter by category">
|
||||||
<option value="">All Categories</option>
|
<option value="">All Categories</option>
|
||||||
{toolsData.filters.categories.map((category) => (
|
{toolsData.filters.categories.map((category) => (
|
||||||
<option value={category}>{category}</option>
|
<option value={category}>{category}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button id="clear-filters" class="btn btn-secondary btn-small"
|
|
||||||
>Clear</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="results-count" class="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} tools</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>
|
||||||
|
|
||||||
<div id="tools-list" role="list" set:html={renderToolsHtml(initialItems)}></div>
|
<div id="tools-list" role="list" set:html={renderToolsHtml(initialItems)}></div>
|
||||||
@@ -64,53 +72,6 @@ const initialItems = toolsData.items.map((item) => ({
|
|||||||
<EmbeddedPageData filename="tools.json" data={toolsData} />
|
<EmbeddedPageData filename="tools.json" data={toolsData} />
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
.search-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: var(--color-card-bg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 0 0 3px rgba(133, 52, 243, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: var(--color-card-bg);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-size: 14px;
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-count {
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
@@ -310,6 +271,7 @@ const initialItems = toolsData.items.map((item) => ({
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/tools';
|
import '../scripts/pages/tools';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="listing-toolbar">
|
<div class="listing-toolbar">
|
||||||
<div class="search-bar">
|
<div class="listing-toolbar-row">
|
||||||
<label for="search-input" class="sr-only">Search workflows</label>
|
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} workflows</div>
|
||||||
<input type="text" id="search-input" placeholder="Search workflows..." autocomplete="off">
|
<details class="listing-controls">
|
||||||
</div>
|
<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="filters-bar" id="filters-bar">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="filter-trigger">Trigger:</label>
|
<label for="filter-trigger">Trigger:</label>
|
||||||
@@ -38,8 +38,9 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
|
|||||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} of {initialItems.length} workflows</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div>
|
<div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div>
|
||||||
<ContributeCTA resourceType="workflows" />
|
<ContributeCTA resourceType="workflows" />
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +52,7 @@ const initialItems = sortWorkflows(workflowsData.items, 'title');
|
|||||||
<EmbeddedPageData filename="workflows.json" data={workflowsData} />
|
<EmbeddedPageData filename="workflows.json" data={workflowsData} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import '../scripts/listing-flyouts';
|
||||||
import '../scripts/pages/workflows';
|
import '../scripts/pages/workflows';
|
||||||
</script>
|
</script>
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
64
website/src/scripts/listing-flyouts.ts
Normal file
64
website/src/scripts/listing-flyouts.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__awesomeCopilotListingFlyoutsInitialized?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLYOUT_SELECTOR = '.listing-controls';
|
||||||
|
|
||||||
|
function closeFlyouts(except?: HTMLDetailsElement): void {
|
||||||
|
document.querySelectorAll<HTMLDetailsElement>(FLYOUT_SELECTOR).forEach((flyout) => {
|
||||||
|
if (flyout !== except) {
|
||||||
|
flyout.open = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initListingFlyouts(): void {
|
||||||
|
if (window.__awesomeCopilotListingFlyoutsInitialized) return;
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
'toggle',
|
||||||
|
(event) => {
|
||||||
|
const flyout = event.target;
|
||||||
|
if (!(flyout instanceof HTMLDetailsElement) || !flyout.matches(FLYOUT_SELECTOR) || !flyout.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFlyouts(flyout);
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target instanceof Element && target.closest(FLYOUT_SELECTOR)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFlyouts();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== 'Escape') return;
|
||||||
|
|
||||||
|
const activeFlyout = document.activeElement instanceof Element
|
||||||
|
? (document.activeElement.closest(FLYOUT_SELECTOR) as HTMLDetailsElement | null)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
closeFlyouts();
|
||||||
|
|
||||||
|
const summary = activeFlyout?.querySelector('summary');
|
||||||
|
if (summary instanceof HTMLElement) {
|
||||||
|
summary.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__awesomeCopilotListingFlyoutsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initListingFlyouts, { once: true });
|
||||||
|
} else {
|
||||||
|
initListingFlyouts();
|
||||||
|
}
|
||||||
@@ -35,36 +35,23 @@ export function sortAgents<T extends RenderableAgent>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderAgentsHtml(
|
export function renderAgentsHtml(items: RenderableAgent[]): string {
|
||||||
items: RenderableAgent[],
|
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
|
||||||
const { query = "", highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No agents found</h3>
|
<h3>No agents found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>No agents are available right now.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.title, query)
|
|
||||||
: escapeHtml(item.title);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.title)}</div>
|
||||||
<div class="resource-description">${escapeHtml(
|
<div class="resource-description">${escapeHtml(
|
||||||
item.description || "No description"
|
item.description || "No description"
|
||||||
)}</div>
|
)}</div>
|
||||||
|
|||||||
@@ -1,109 +1,48 @@
|
|||||||
/**
|
/**
|
||||||
* Agents page functionality
|
* Agents page functionality
|
||||||
*/
|
*/
|
||||||
import {
|
|
||||||
createChoices,
|
|
||||||
getChoicesValues,
|
|
||||||
setChoicesValues,
|
|
||||||
type Choices,
|
|
||||||
} from '../choices';
|
|
||||||
import { FuzzySearch, type SearchItem } from '../search';
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamFlag,
|
|
||||||
getQueryParamValues,
|
|
||||||
setupDropdownCloseHandlers,
|
setupDropdownCloseHandlers,
|
||||||
setupActionHandlers,
|
setupActionHandlers,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { setupModal, openFileModal } from '../modal';
|
import { setupModal, openFileModal } from '../modal';
|
||||||
import { renderAgentsHtml, sortAgents, type AgentSortOption, type RenderableAgent } from './agents-render';
|
import {
|
||||||
|
renderAgentsHtml,
|
||||||
|
sortAgents,
|
||||||
|
type AgentSortOption,
|
||||||
|
type RenderableAgent,
|
||||||
|
} from './agents-render';
|
||||||
|
|
||||||
interface Agent extends SearchItem, RenderableAgent {
|
interface Agent extends RenderableAgent {
|
||||||
model?: string | string[];
|
|
||||||
tools?: string[];
|
|
||||||
hasHandoffs?: boolean;
|
|
||||||
lastUpdated?: string | null;
|
lastUpdated?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AgentsData {
|
interface AgentsData {
|
||||||
items: Agent[];
|
items: Agent[];
|
||||||
filters: {
|
|
||||||
models: string[];
|
|
||||||
tools: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let allItems: Agent[] = [];
|
let allItems: Agent[] = [];
|
||||||
let search = new FuzzySearch<Agent>();
|
|
||||||
let modelSelect: Choices;
|
|
||||||
let toolSelect: Choices;
|
|
||||||
let currentSort: AgentSortOption = 'title';
|
let currentSort: AgentSortOption = 'title';
|
||||||
let resourceListHandlersReady = false;
|
let resourceListHandlersReady = false;
|
||||||
|
|
||||||
let currentFilters = {
|
|
||||||
models: [] as string[],
|
|
||||||
tools: [] as string[],
|
|
||||||
hasHandoffs: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function sortItems(items: Agent[]): Agent[] {
|
|
||||||
return sortAgents(items, currentSort);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById('results-count');
|
const countEl = document.getElementById('results-count');
|
||||||
const query = searchInput?.value || '';
|
const results = sortAgents(allItems, currentSort);
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
renderItems(results);
|
||||||
|
if (countEl) {
|
||||||
if (currentFilters.models.length > 0) {
|
countEl.textContent = `${results.length} agent${results.length === 1 ? '' : 's'}`;
|
||||||
results = results.filter(item => {
|
|
||||||
if (currentFilters.models.includes('(none)') && !item.model) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return item.model && (Array.isArray(item.model) ? item.model.some(m => currentFilters.models.includes(m)) : currentFilters.models.includes(item.model));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFilters.tools.length > 0) {
|
|
||||||
results = results.filter(item =>
|
|
||||||
item.tools?.some(tool => currentFilters.tools.includes(tool))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFilters.hasHandoffs) {
|
|
||||||
results = results.filter(item => item.hasHandoffs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply sorting
|
|
||||||
results = sortItems(results);
|
|
||||||
|
|
||||||
renderItems(results, query);
|
|
||||||
|
|
||||||
const activeFilters: string[] = [];
|
|
||||||
if (currentFilters.models.length > 0) activeFilters.push(`models: ${currentFilters.models.length}`);
|
|
||||||
if (currentFilters.tools.length > 0) activeFilters.push(`tools: ${currentFilters.tools.length}`);
|
|
||||||
if (currentFilters.hasHandoffs) activeFilters.push('has handoffs');
|
|
||||||
|
|
||||||
let countText = `${results.length} of ${allItems.length} agents`;
|
|
||||||
if (activeFilters.length > 0) {
|
|
||||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
|
||||||
}
|
|
||||||
if (countEl) countEl.textContent = countText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Agent[], query = ''): void {
|
function renderItems(items: Agent[]): void {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderAgentsHtml(items, {
|
list.innerHTML = renderAgentsHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) => search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -125,21 +64,18 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? '',
|
q: '',
|
||||||
model: currentFilters.models,
|
model: [],
|
||||||
tool: currentFilters.tools,
|
tool: [],
|
||||||
handoffs: currentFilters.hasHandoffs,
|
handoffs: false,
|
||||||
sort: currentSort === 'title' ? '' : currentSort,
|
sort: currentSort === 'title' ? '' : currentSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initAgentsPage(): Promise<void> {
|
export async function initAgentsPage(): Promise<void> {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
||||||
const handoffsCheckbox = document.getElementById('filter-handoffs') as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
|
||||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||||
|
|
||||||
setupResourceListHandlers(list as HTMLElement | null);
|
setupResourceListHandlers(list as HTMLElement | null);
|
||||||
@@ -151,84 +87,17 @@ export async function initAgentsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
// Initialize Choices.js for model filter
|
|
||||||
modelSelect = createChoices('#filter-model', { placeholderValue: 'All Models' });
|
|
||||||
modelSelect.setChoices(data.filters.models.map(m => ({ value: m, label: m })), 'value', 'label', true);
|
|
||||||
|
|
||||||
const initialQuery = getQueryParam('q');
|
|
||||||
const initialModels = getQueryParamValues('model').filter(model => data.filters.models.includes(model));
|
|
||||||
const initialTools = getQueryParamValues('tool').filter(tool => data.filters.tools.includes(tool));
|
|
||||||
const initialSort = getQueryParam('sort');
|
const initialSort = getQueryParam('sort');
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialModels.length > 0) {
|
|
||||||
currentFilters.models = initialModels;
|
|
||||||
setChoicesValues(modelSelect, initialModels);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('filter-model')?.addEventListener('change', () => {
|
|
||||||
currentFilters.models = getChoicesValues(modelSelect);
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize Choices.js for tool filter
|
|
||||||
toolSelect = createChoices('#filter-tool', { placeholderValue: 'All Tools' });
|
|
||||||
toolSelect.setChoices(data.filters.tools.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
|
||||||
if (initialTools.length > 0) {
|
|
||||||
currentFilters.tools = initialTools;
|
|
||||||
setChoicesValues(toolSelect, initialTools);
|
|
||||||
}
|
|
||||||
document.getElementById('filter-tool')?.addEventListener('change', () => {
|
|
||||||
currentFilters.tools = getChoicesValues(toolSelect);
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize sort select
|
|
||||||
if (initialSort === 'lastUpdated') {
|
if (initialSort === 'lastUpdated') {
|
||||||
currentSort = initialSort;
|
currentSort = initialSort;
|
||||||
if (sortSelect) sortSelect.value = initialSort;
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
sortSelect?.addEventListener('change', () => {
|
sortSelect?.addEventListener('change', () => {
|
||||||
currentSort = sortSelect.value as AgentSortOption;
|
currentSort = sortSelect.value as AgentSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
|
||||||
|
|
||||||
const countEl = document.getElementById('results-count');
|
|
||||||
if (countEl) {
|
|
||||||
countEl.textContent = `${allItems.length} of ${allItems.length} agents`;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchInput?.addEventListener('input', debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200));
|
|
||||||
|
|
||||||
if (getQueryParamFlag('handoffs')) {
|
|
||||||
currentFilters.hasHandoffs = true;
|
|
||||||
if (handoffsCheckbox) handoffsCheckbox.checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handoffsCheckbox?.addEventListener('change', () => {
|
|
||||||
currentFilters.hasHandoffs = handoffsCheckbox.checked;
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener('click', () => {
|
|
||||||
currentFilters = { models: [], tools: [], hasHandoffs: false };
|
|
||||||
currentSort = 'title';
|
|
||||||
modelSelect.removeActiveItems();
|
|
||||||
toolSelect.removeActiveItems();
|
|
||||||
if (handoffsCheckbox) handoffsCheckbox.checked = false;
|
|
||||||
if (searchInput) searchInput.value = '';
|
|
||||||
if (sortSelect) sortSelect.value = 'title';
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
|||||||
@@ -33,38 +33,25 @@ export function sortHooks<T extends RenderableHook>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderHooksHtml(
|
export function renderHooksHtml(items: RenderableHook[]): string {
|
||||||
items: RenderableHook[],
|
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
|
||||||
const { query = "", highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No hooks found</h3>
|
<h3>No hooks found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>Try adjusting the selected filters.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.title, query)
|
|
||||||
: escapeHtml(item.title);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item" data-path="${escapeHtml(
|
<article class="resource-item" data-path="${escapeHtml(
|
||||||
item.readmeFile
|
item.readmeFile
|
||||||
)}" data-hook-id="${escapeHtml(item.id)}" role="listitem">
|
)}" data-hook-id="${escapeHtml(item.id)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.title)}</div>
|
||||||
<div class="resource-description">${escapeHtml(
|
<div class="resource-description">${escapeHtml(
|
||||||
item.description || "No description"
|
item.description || "No description"
|
||||||
)}</div>
|
)}</div>
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {
|
|||||||
setChoicesValues,
|
setChoicesValues,
|
||||||
type Choices,
|
type Choices,
|
||||||
} from "../choices";
|
} from "../choices";
|
||||||
import { FuzzySearch, type SearchItem } from "../search";
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamValues,
|
getQueryParamValues,
|
||||||
showToast,
|
showToast,
|
||||||
@@ -25,23 +23,19 @@ import {
|
|||||||
type RenderableHook,
|
type RenderableHook,
|
||||||
} from "./hooks-render";
|
} from "./hooks-render";
|
||||||
|
|
||||||
interface Hook extends SearchItem, RenderableHook {}
|
interface Hook extends RenderableHook {}
|
||||||
|
|
||||||
interface HooksData {
|
interface HooksData {
|
||||||
items: Hook[];
|
items: Hook[];
|
||||||
filters: {
|
filters: {
|
||||||
hooks: string[];
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceType = "hook";
|
const resourceType = "hook";
|
||||||
let allItems: Hook[] = [];
|
let allItems: Hook[] = [];
|
||||||
let search = new FuzzySearch<Hook>();
|
|
||||||
let hookSelect: Choices;
|
|
||||||
let tagSelect: Choices;
|
let tagSelect: Choices;
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
hooks: [] as string[],
|
|
||||||
tags: [] as string[],
|
tags: [] as string[],
|
||||||
};
|
};
|
||||||
let currentSort: HookSortOption = "title";
|
let currentSort: HookSortOption = "title";
|
||||||
@@ -52,57 +46,30 @@ function sortItems(items: Hook[]): Hook[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById("results-count");
|
const countEl = document.getElementById("results-count");
|
||||||
const query = searchInput?.value || "";
|
let results = [...allItems];
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
|
||||||
|
|
||||||
if (currentFilters.hooks.length > 0) {
|
|
||||||
results = results.filter((item) =>
|
|
||||||
item.hooks.some((h) => currentFilters.hooks.includes(h))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (currentFilters.tags.length > 0) {
|
if (currentFilters.tags.length > 0) {
|
||||||
results = results.filter((item) =>
|
results = results.filter((item) =>
|
||||||
item.tags.some((t) => currentFilters.tags.includes(t))
|
item.tags.some((tag) => currentFilters.tags.includes(tag))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
results = sortItems(results);
|
results = sortItems(results);
|
||||||
|
|
||||||
renderItems(results, query);
|
renderItems(results);
|
||||||
const activeFilters: string[] = [];
|
let countText = `${results.length} hook${results.length === 1 ? "" : "s"}`;
|
||||||
if (currentFilters.hooks.length > 0)
|
if (currentFilters.tags.length > 0) {
|
||||||
activeFilters.push(
|
countText = `${results.length} of ${allItems.length} hooks (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? "s" : ""})`;
|
||||||
`${currentFilters.hooks.length} hook event${
|
|
||||||
currentFilters.hooks.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} hooks`;
|
|
||||||
if (activeFilters.length > 0) {
|
|
||||||
countText += ` (filtered by ${activeFilters.join(", ")})`;
|
|
||||||
}
|
}
|
||||||
if (countEl) countEl.textContent = countText;
|
if (countEl) countEl.textContent = countText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Hook[], query = ""): void {
|
function renderItems(items: Hook[]): void {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderHooksHtml(items, {
|
list.innerHTML = renderHooksHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) =>
|
|
||||||
search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -134,10 +101,10 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? "",
|
q: "",
|
||||||
hook: currentFilters.hooks,
|
hook: [],
|
||||||
tag: currentFilters.tags,
|
tag: currentFilters.tags,
|
||||||
sort: currentSort === "title" ? "" : currentSort,
|
sort: currentSort === "title" ? "" : currentSort,
|
||||||
});
|
});
|
||||||
@@ -153,12 +120,11 @@ async function downloadHook(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build file list: README.md + all assets
|
|
||||||
const files = [
|
const files = [
|
||||||
{ name: "README.md", path: hook.readmeFile },
|
{ name: "README.md", path: hook.readmeFile },
|
||||||
...hook.assets.map((a) => ({
|
...hook.assets.map((asset) => ({
|
||||||
name: a,
|
name: asset,
|
||||||
path: `${hook.path}/${a}`,
|
path: `${hook.path}/${asset}`,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -196,9 +162,6 @@ async function downloadHook(
|
|||||||
|
|
||||||
export async function initHooksPage(): Promise<void> {
|
export async function initHooksPage(): Promise<void> {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
const sortSelect = document.getElementById(
|
const sortSelect = document.getElementById(
|
||||||
"sort-select"
|
"sort-select"
|
||||||
@@ -215,90 +178,53 @@ export async function initHooksPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
// Setup hook event filter
|
tagSelect = createChoices("#filter-tag", {
|
||||||
hookSelect = createChoices("#filter-hook", {
|
placeholderValue: "All Tags",
|
||||||
placeholderValue: "All Events",
|
|
||||||
});
|
});
|
||||||
hookSelect.setChoices(
|
tagSelect.setChoices(
|
||||||
data.filters.hooks.map((h) => ({ value: h, label: h })),
|
data.filters.tags.map((tag) => ({ value: tag, label: tag })),
|
||||||
"value",
|
"value",
|
||||||
"label",
|
"label",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialQuery = getQueryParam("q");
|
|
||||||
const initialHooks = getQueryParamValues("hook").filter((hook) =>
|
|
||||||
data.filters.hooks.includes(hook)
|
|
||||||
);
|
|
||||||
const initialTags = getQueryParamValues("tag").filter((tag) =>
|
const initialTags = getQueryParamValues("tag").filter((tag) =>
|
||||||
data.filters.tags.includes(tag)
|
data.filters.tags.includes(tag)
|
||||||
);
|
);
|
||||||
const initialSort = getQueryParam("sort");
|
const initialSort = getQueryParam("sort");
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialHooks.length > 0) {
|
|
||||||
currentFilters.hooks = initialHooks;
|
|
||||||
setChoicesValues(hookSelect, initialHooks);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("filter-hook")?.addEventListener("change", () => {
|
|
||||||
currentFilters.hooks = getChoicesValues(hookSelect);
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup tag filter
|
|
||||||
tagSelect = createChoices("#filter-tag", {
|
|
||||||
placeholderValue: "All Tags",
|
|
||||||
});
|
|
||||||
tagSelect.setChoices(
|
|
||||||
data.filters.tags.map((t) => ({ value: t, label: t })),
|
|
||||||
"value",
|
|
||||||
"label",
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (initialTags.length > 0) {
|
if (initialTags.length > 0) {
|
||||||
currentFilters.tags = initialTags;
|
currentFilters.tags = initialTags;
|
||||||
setChoicesValues(tagSelect, initialTags);
|
setChoicesValues(tagSelect, initialTags);
|
||||||
}
|
}
|
||||||
document.getElementById("filter-tag")?.addEventListener("change", () => {
|
|
||||||
currentFilters.tags = getChoicesValues(tagSelect);
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (initialSort === "lastUpdated") {
|
if (initialSort === "lastUpdated") {
|
||||||
currentSort = initialSort;
|
currentSort = initialSort;
|
||||||
if (sortSelect) sortSelect.value = initialSort;
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById("filter-tag")?.addEventListener("change", () => {
|
||||||
|
currentFilters.tags = getChoicesValues(tagSelect);
|
||||||
|
applyFiltersAndRender();
|
||||||
|
syncUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
sortSelect?.addEventListener("change", () => {
|
sortSelect?.addEventListener("change", () => {
|
||||||
currentSort = sortSelect.value as HookSortOption;
|
currentSort = sortSelect.value as HookSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
|
||||||
searchInput?.addEventListener(
|
|
||||||
"input",
|
|
||||||
debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200)
|
|
||||||
);
|
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener("click", () => {
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
currentFilters = { hooks: [], tags: [] };
|
currentFilters = { tags: [] };
|
||||||
currentSort = "title";
|
currentSort = "title";
|
||||||
hookSelect.removeActiveItems();
|
|
||||||
tagSelect.removeActiveItems();
|
tagSelect.removeActiveItems();
|
||||||
if (searchInput) searchInput.value = "";
|
|
||||||
if (sortSelect) sortSelect.value = "title";
|
if (sortSelect) sortSelect.value = "title";
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
applyFiltersAndRender();
|
||||||
setupModal();
|
setupModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Homepage functionality
|
* Homepage functionality
|
||||||
*/
|
*/
|
||||||
import { FuzzySearch, type SearchItem } from '../search';
|
import { fetchData } from "../utils";
|
||||||
import { fetchData, debounce, escapeHtml, truncate, getResourceIcon } from '../utils';
|
|
||||||
import { setupModal, openFileModal } from '../modal';
|
|
||||||
|
|
||||||
// SVG icon definitions for search results
|
|
||||||
// Icons with `fill: true` use fill="currentColor", others use stroke
|
|
||||||
const iconDefs: Record<string, { path: string; fill?: boolean }> = {
|
|
||||||
// Agent icon - GitHub Primer's agent-24
|
|
||||||
robot: {
|
|
||||||
fill: true,
|
|
||||||
path: '<path d="M22.5 13.919v-.278a5.097 5.097 0 0 0-4.961-5.086.858.858 0 0 1-.754-.497l-.149-.327A6.414 6.414 0 0 0 10.81 4a6.133 6.133 0 0 0-6.13 6.32l.019.628a.863.863 0 0 1-.67.869A3.263 3.263 0 0 0 1.5 14.996v.108A3.397 3.397 0 0 0 4.896 18.5h1.577a.75.75 0 0 1 0 1.5H4.896A4.896 4.896 0 0 1 0 15.104v-.108a4.761 4.761 0 0 1 3.185-4.493l-.004-.137A7.633 7.633 0 0 1 10.81 2.5a7.911 7.911 0 0 1 7.176 4.58C21.36 7.377 24 10.207 24 13.641v.278a.75.75 0 0 1-1.5 0Z"/><path d="m12.306 11.77 3.374 3.375a.749.749 0 0 1 0 1.061l-3.375 3.375-.057.051a.751.751 0 0 1-1.004-.051.751.751 0 0 1-.051-1.004l.051-.057 2.845-2.845-2.844-2.844a.75.75 0 1 1 1.061-1.061ZM22.5 19.8H18a.75.75 0 0 1 0-1.5h4.5a.75.75 0 0 1 0 1.5Z"/>'
|
|
||||||
},
|
|
||||||
document: {
|
|
||||||
path: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
||||||
},
|
|
||||||
lightning: {
|
|
||||||
path: '<path d="M13 2 4.09 12.11a1.23 1.23 0 0 0 .13 1.72l.16.14a1.23 1.23 0 0 0 1.52 0L13 9.5V22l8.91-10.11a1.23 1.23 0 0 0-.13-1.72l-.16-.14a1.23 1.23 0 0 0-1.52 0L13 14.5V2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
||||||
},
|
|
||||||
// Hook icon - GitHub Primer's sync-24
|
|
||||||
hook: {
|
|
||||||
fill: true,
|
|
||||||
path: '<path d="M3.38 8A9.502 9.502 0 0 1 12 2.5a9.502 9.502 0 0 1 9.215 7.182.75.75 0 1 0 1.456-.364C21.473 4.539 17.15 1 12 1a10.995 10.995 0 0 0-9.5 5.452V4.75a.75.75 0 0 0-1.5 0V8.5a1 1 0 0 0 1 1h3.75a.75.75 0 0 0 0-1.5H3.38Zm-.595 6.318a.75.75 0 0 0-1.455.364C2.527 19.461 6.85 23 12 23c4.052 0 7.592-2.191 9.5-5.451v1.701a.75.75 0 0 0 1.5 0V15.5a1 1 0 0 0-1-1h-3.75a.75.75 0 0 0 0 1.5h2.37A9.502 9.502 0 0 1 12 21.5c-4.446 0-8.181-3.055-9.215-7.182Z"/>'
|
|
||||||
},
|
|
||||||
// Workflow icon - GitHub Primer's workflow-24
|
|
||||||
workflow: {
|
|
||||||
fill: true,
|
|
||||||
path: '<path d="M1 3a2 2 0 0 1 2-2h6.5a2 2 0 0 1 2 2v6.5a2 2 0 0 1-2 2H7v4.063C7 16.355 7.644 17 8.438 17H12.5v-2.5a2 2 0 0 1 2-2H21a2 2 0 0 1 2 2V21a2 2 0 0 1-2 2h-6.5a2 2 0 0 1-2-2v-2.5H8.437A2.939 2.939 0 0 1 5.5 15.562V11.5H3a2 2 0 0 1-2-2Zm2-.5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 0 .5.5h6.5a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5ZM14.5 14a.5.5 0 0 0-.5.5V21a.5.5 0 0 0 .5.5H21a.5.5 0 0 0 .5-.5v-6.5a.5.5 0 0 0-.5-.5Z"/>'
|
|
||||||
},
|
|
||||||
// Plug icon - GitHub Primer's plug-24
|
|
||||||
plug: {
|
|
||||||
fill: true,
|
|
||||||
path: '<path d="M7 11.5H2.938c-.794 0-1.438.644-1.438 1.437v8.313a.75.75 0 0 1-1.5 0v-8.312A2.939 2.939 0 0 1 2.937 10H7V6.151c0-.897.678-1.648 1.57-1.74l6.055-.626 1.006-1.174A1.752 1.752 0 0 1 16.96 2h1.29c.966 0 1.75.784 1.75 1.75V6h3.25a.75.75 0 0 1 0 1.5H20V14h3.25a.75.75 0 0 1 0 1.5H20v2.25a1.75 1.75 0 0 1-1.75 1.75h-1.29a1.75 1.75 0 0 1-1.329-.611l-1.006-1.174-6.055-.627A1.749 1.749 0 0 1 7 15.348Zm9.77-7.913v.001l-1.201 1.4a.75.75 0 0 1-.492.258l-6.353.657a.25.25 0 0 0-.224.249v9.196a.25.25 0 0 0 .224.249l6.353.657c.191.02.368.112.493.258l1.2 1.401a.252.252 0 0 0 .19.087h1.29a.25.25 0 0 0 .25-.25v-14a.25.25 0 0 0-.25-.25h-1.29a.252.252 0 0 0-.19.087Z"/>'
|
|
||||||
},
|
|
||||||
wrench: {
|
|
||||||
path: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getIconSvg(iconName: string): string {
|
|
||||||
const icon = iconDefs[iconName] || iconDefs.document;
|
|
||||||
const fill = icon.fill ? 'fill="currentColor"' : 'fill="none"';
|
|
||||||
return `<svg viewBox="0 0 24 24" ${fill} aria-hidden="true">${icon.path}</svg>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Manifest {
|
interface Manifest {
|
||||||
counts: {
|
counts: {
|
||||||
@@ -57,301 +15,30 @@ interface Manifest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Plugin {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
path: string;
|
|
||||||
tags?: string[];
|
|
||||||
featured?: boolean;
|
|
||||||
itemCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginsData {
|
|
||||||
items: Plugin[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recent searches storage
|
|
||||||
const RECENT_SEARCHES_KEY = 'awesome-copilot-recent-searches';
|
|
||||||
const MAX_RECENT_SEARCHES = 5;
|
|
||||||
|
|
||||||
function getRecentSearches(): string[] {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
|
|
||||||
return stored ? JSON.parse(stored) : [];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRecentSearch(query: string): void {
|
|
||||||
if (!query.trim()) return;
|
|
||||||
const searches = getRecentSearches();
|
|
||||||
const filtered = searches.filter(s => s.toLowerCase() !== query.toLowerCase());
|
|
||||||
filtered.unshift(query);
|
|
||||||
const limited = filtered.slice(0, MAX_RECENT_SEARCHES);
|
|
||||||
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limited));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRecentSearch(query: string): void {
|
|
||||||
const searches = getRecentSearches();
|
|
||||||
const filtered = searches.filter(s => s !== query);
|
|
||||||
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(filtered));
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearRecentSearches(): void {
|
|
||||||
localStorage.removeItem(RECENT_SEARCHES_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initHomepage(): Promise<void> {
|
export async function initHomepage(): Promise<void> {
|
||||||
// Load manifest for stats
|
// Load manifest for stats
|
||||||
const manifest = await fetchData<Manifest>('manifest.json');
|
const manifest = await fetchData<Manifest>("manifest.json");
|
||||||
if (manifest && manifest.counts) {
|
if (manifest && manifest.counts) {
|
||||||
// Populate counts in cards
|
// Populate counts in cards
|
||||||
const countKeys = ['agents', 'instructions', 'skills', 'hooks', 'workflows', 'plugins', 'tools'] as const;
|
const countKeys = [
|
||||||
countKeys.forEach(key => {
|
"agents",
|
||||||
const countEl = document.querySelector(`.card-count[data-count="${key}"]`);
|
"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) {
|
if (countEl && manifest.counts[key] !== undefined) {
|
||||||
countEl.textContent = manifest.counts[key].toString();
|
countEl.textContent = manifest.counts[key].toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load search index
|
|
||||||
const searchIndex = await fetchData<SearchItem[]>('search-index.json');
|
|
||||||
if (searchIndex) {
|
|
||||||
const search = new FuzzySearch<SearchItem>();
|
|
||||||
search.setItems(searchIndex);
|
|
||||||
|
|
||||||
const searchInput = document.getElementById('global-search') as HTMLInputElement;
|
|
||||||
const resultsDiv = document.getElementById('search-results');
|
|
||||||
|
|
||||||
if (searchInput && resultsDiv) {
|
|
||||||
const statusEl = document.getElementById("global-search-status");
|
|
||||||
let isShowingRecent = false;
|
|
||||||
|
|
||||||
const hideResults = (): void => {
|
|
||||||
resultsDiv.classList.add("hidden");
|
|
||||||
isShowingRecent = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showResults = (): void => {
|
|
||||||
resultsDiv.classList.remove("hidden");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getResultButtons = (): HTMLButtonElement[] =>
|
|
||||||
Array.from(
|
|
||||||
resultsDiv.querySelectorAll<HTMLButtonElement>(".search-result, .search-recent-item")
|
|
||||||
);
|
|
||||||
|
|
||||||
const openResult = (resultEl: HTMLElement): void => {
|
|
||||||
const path = resultEl.dataset.path;
|
|
||||||
const type = resultEl.dataset.type;
|
|
||||||
if (path && type) {
|
|
||||||
hideResults();
|
|
||||||
openFileModal(path, type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render recent searches
|
|
||||||
const renderRecentSearches = (): void => {
|
|
||||||
const recent = getRecentSearches();
|
|
||||||
if (recent.length === 0) return;
|
|
||||||
|
|
||||||
const clockIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`;
|
|
||||||
const xIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
||||||
|
|
||||||
resultsDiv.innerHTML = `
|
|
||||||
<div class="search-recent-header">
|
|
||||||
<span>Recent Searches</span>
|
|
||||||
<button class="search-clear-recent" aria-label="Clear recent searches">Clear</button>
|
|
||||||
</div>
|
|
||||||
${recent.map(query => `
|
|
||||||
<button type="button" class="search-recent-item" data-query="${escapeHtml(query)}">
|
|
||||||
<span class="search-recent-icon">${clockIcon}</span>
|
|
||||||
<span class="search-recent-text">${escapeHtml(query)}</span>
|
|
||||||
<button type="button" class="search-recent-remove" data-query="${escapeHtml(query)}" aria-label="Remove from history">
|
|
||||||
${xIcon}
|
|
||||||
</button>
|
|
||||||
</button>
|
|
||||||
`).join('')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add click handlers for recent items
|
|
||||||
resultsDiv.querySelectorAll('.search-recent-item').forEach(item => {
|
|
||||||
item.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('.search-recent-remove')) return;
|
|
||||||
const query = (item as HTMLElement).dataset.query;
|
|
||||||
if (query) {
|
|
||||||
searchInput.value = query;
|
|
||||||
searchInput.dispatchEvent(new Event('input'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add click handlers for remove buttons
|
|
||||||
resultsDiv.querySelectorAll('.search-recent-remove').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const query = (btn as HTMLElement).dataset.query;
|
|
||||||
if (query) {
|
|
||||||
removeRecentSearch(query);
|
|
||||||
renderRecentSearches();
|
|
||||||
if (getRecentSearches().length === 0) {
|
|
||||||
hideResults();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add clear all handler
|
|
||||||
const clearBtn = resultsDiv.querySelector('.search-clear-recent');
|
|
||||||
clearBtn?.addEventListener('click', () => {
|
|
||||||
clearRecentSearches();
|
|
||||||
hideResults();
|
|
||||||
});
|
|
||||||
|
|
||||||
isShowingRecent = true;
|
|
||||||
showResults();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show recent searches on focus when empty
|
|
||||||
searchInput.addEventListener('focus', () => {
|
|
||||||
if (searchInput.value.trim().length === 0) {
|
|
||||||
renderRecentSearches();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchInput.addEventListener('input', debounce(() => {
|
|
||||||
const query = searchInput.value.trim();
|
|
||||||
if (query.length < 2) {
|
|
||||||
if (query.length === 0) {
|
|
||||||
renderRecentSearches();
|
|
||||||
} else {
|
|
||||||
resultsDiv.innerHTML = '';
|
|
||||||
hideResults();
|
|
||||||
}
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.textContent = '';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isShowingRecent = false;
|
|
||||||
const results = search.search(query).slice(0, 10);
|
|
||||||
if (results.length === 0) {
|
|
||||||
resultsDiv.innerHTML = `
|
|
||||||
<div class="search-result-empty">
|
|
||||||
<div class="search-result-empty-icon">
|
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="11" cy="11" r="8"/>
|
|
||||||
<path d="M21 21l-4.35-4.35"/>
|
|
||||||
<path d="M8 8l6 6M14 8l-6 6"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="search-result-empty-title">No results found</div>
|
|
||||||
<div class="search-result-empty-hint">Try different keywords or check your spelling</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.textContent = 'No results found.';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Add to recent searches when user gets results
|
|
||||||
addRecentSearch(query);
|
|
||||||
|
|
||||||
resultsDiv.innerHTML = results.map(item => {
|
|
||||||
const iconName = getResourceIcon(item.type);
|
|
||||||
return `
|
|
||||||
<button type="button" class="search-result" data-path="${escapeHtml(item.path)}" data-type="${escapeHtml(item.type)}">
|
|
||||||
<span class="search-result-type" data-icon="${iconName}">${getIconSvg(iconName)}</span>
|
|
||||||
<div>
|
|
||||||
<div class="search-result-title">${search.highlight(item.title, query)}</div>
|
|
||||||
<div class="search-result-description">${truncate(item.description, 60)}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
`}).join('');
|
|
||||||
|
|
||||||
if (statusEl) {
|
|
||||||
statusEl.textContent = `${results.length} result${results.length === 1 ? '' : 's'} available.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getResultButtons().forEach((el, index, buttons) => {
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
openResult(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener("keydown", (event) => {
|
|
||||||
switch (event.key) {
|
|
||||||
case "ArrowDown":
|
|
||||||
event.preventDefault();
|
|
||||||
buttons[(index + 1) % buttons.length]?.focus();
|
|
||||||
break;
|
|
||||||
case "ArrowUp":
|
|
||||||
event.preventDefault();
|
|
||||||
if (index === 0) {
|
|
||||||
searchInput.focus();
|
|
||||||
} else {
|
|
||||||
buttons[index - 1]?.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "Home":
|
|
||||||
event.preventDefault();
|
|
||||||
buttons[0]?.focus();
|
|
||||||
break;
|
|
||||||
case "End":
|
|
||||||
event.preventDefault();
|
|
||||||
buttons[buttons.length - 1]?.focus();
|
|
||||||
break;
|
|
||||||
case "Escape":
|
|
||||||
event.preventDefault();
|
|
||||||
hideResults();
|
|
||||||
searchInput.focus();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showResults();
|
|
||||||
}, 200));
|
|
||||||
|
|
||||||
searchInput.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "ArrowDown") {
|
|
||||||
const firstResult = getResultButtons()[0];
|
|
||||||
if (firstResult) {
|
|
||||||
event.preventDefault();
|
|
||||||
firstResult.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
hideResults();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close results when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!searchInput.contains(e.target as Node) && !resultsDiv.contains(e.target as Node)) {
|
|
||||||
hideResults();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cmd/Ctrl + K to focus search
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
searchInput.focus();
|
|
||||||
searchInput.select();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup modal
|
|
||||||
setupModal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-initialize when DOM is ready
|
// Auto-initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initHomepage);
|
document.addEventListener("DOMContentLoaded", initHomepage);
|
||||||
|
|||||||
@@ -33,19 +33,13 @@ export function sortInstructions<T extends RenderableInstruction>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderInstructionsHtml(
|
export function renderInstructionsHtml(
|
||||||
items: RenderableInstruction[],
|
items: RenderableInstruction[]
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
): string {
|
||||||
const { query = '', highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No instructions found</h3>
|
<h3>No instructions found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>Try adjusting the selected filters.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -55,16 +49,12 @@ export function renderInstructionsHtml(
|
|||||||
const applyToText = Array.isArray(item.applyTo)
|
const applyToText = Array.isArray(item.applyTo)
|
||||||
? item.applyTo.join(', ')
|
? item.applyTo.join(', ')
|
||||||
: item.applyTo;
|
: item.applyTo;
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.title, query)
|
|
||||||
: escapeHtml(item.title);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.title)}</div>
|
||||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
${applyToText ? `<span class="resource-tag">applies to: ${escapeHtml(applyToText)}</span>` : ''}
|
${applyToText ? `<span class="resource-tag">applies to: ${escapeHtml(applyToText)}</span>` : ''}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {
|
|||||||
setChoicesValues,
|
setChoicesValues,
|
||||||
type Choices,
|
type Choices,
|
||||||
} from '../choices';
|
} from '../choices';
|
||||||
import { FuzzySearch, type SearchItem } from '../search';
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamValues,
|
getQueryParamValues,
|
||||||
setupDropdownCloseHandlers,
|
setupDropdownCloseHandlers,
|
||||||
@@ -25,7 +23,7 @@ import {
|
|||||||
type RenderableInstruction,
|
type RenderableInstruction,
|
||||||
} from './instructions-render';
|
} from './instructions-render';
|
||||||
|
|
||||||
interface Instruction extends SearchItem, RenderableInstruction {
|
interface Instruction extends RenderableInstruction {
|
||||||
path: string;
|
path: string;
|
||||||
applyTo?: string | string[];
|
applyTo?: string | string[];
|
||||||
extensions?: string[];
|
extensions?: string[];
|
||||||
@@ -41,7 +39,6 @@ interface InstructionsData {
|
|||||||
|
|
||||||
const resourceType = 'instruction';
|
const resourceType = 'instruction';
|
||||||
let allItems: Instruction[] = [];
|
let allItems: Instruction[] = [];
|
||||||
let search = new FuzzySearch<Instruction>();
|
|
||||||
let extensionSelect: Choices;
|
let extensionSelect: Choices;
|
||||||
let currentFilters = { extensions: [] as string[] };
|
let currentFilters = { extensions: [] as string[] };
|
||||||
let currentSort: InstructionSortOption = 'title';
|
let currentSort: InstructionSortOption = 'title';
|
||||||
@@ -52,11 +49,8 @@ function sortItems(items: Instruction[]): Instruction[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById('results-count');
|
const countEl = document.getElementById('results-count');
|
||||||
const query = searchInput?.value || '';
|
let results = [...allItems];
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
|
||||||
|
|
||||||
if (currentFilters.extensions.length > 0) {
|
if (currentFilters.extensions.length > 0) {
|
||||||
results = results.filter(item => {
|
results = results.filter(item => {
|
||||||
@@ -69,22 +63,19 @@ function applyFiltersAndRender(): void {
|
|||||||
|
|
||||||
results = sortItems(results);
|
results = sortItems(results);
|
||||||
|
|
||||||
renderItems(results, query);
|
renderItems(results);
|
||||||
let countText = `${results.length} of ${allItems.length} instructions`;
|
let countText = `${results.length} instruction${results.length === 1 ? '' : 's'}`;
|
||||||
if (currentFilters.extensions.length > 0) {
|
if (currentFilters.extensions.length > 0) {
|
||||||
countText += ` (filtered by ${currentFilters.extensions.length} extension${currentFilters.extensions.length > 1 ? 's' : ''})`;
|
countText = `${results.length} of ${allItems.length} instructions (filtered by ${currentFilters.extensions.length} extension${currentFilters.extensions.length > 1 ? 's' : ''})`;
|
||||||
}
|
}
|
||||||
if (countEl) countEl.textContent = countText;
|
if (countEl) countEl.textContent = countText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Instruction[], query = ''): void {
|
function renderItems(items: Instruction[]): void {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderInstructionsHtml(items, {
|
list.innerHTML = renderInstructionsHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) => search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -106,9 +97,9 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? '',
|
q: '',
|
||||||
extension: currentFilters.extensions,
|
extension: currentFilters.extensions,
|
||||||
sort: currentSort === 'title' ? '' : currentSort,
|
sort: currentSort === 'title' ? '' : currentSort,
|
||||||
});
|
});
|
||||||
@@ -116,7 +107,6 @@ function syncUrlState(searchInput: HTMLInputElement | null): void {
|
|||||||
|
|
||||||
export async function initInstructionsPage(): Promise<void> {
|
export async function initInstructionsPage(): Promise<void> {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||||
|
|
||||||
@@ -129,16 +119,13 @@ export async function initInstructionsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
|
extensionSelect = createChoices('#filter-extension', { placeholderValue: 'All Extensions' });
|
||||||
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
|
extensionSelect.setChoices(data.filters.extensions.map(e => ({ value: e, label: e })), 'value', 'label', true);
|
||||||
|
|
||||||
const initialQuery = getQueryParam('q');
|
|
||||||
const initialExtensions = getQueryParamValues('extension').filter(extension => data.filters.extensions.includes(extension));
|
const initialExtensions = getQueryParamValues('extension').filter(extension => data.filters.extensions.includes(extension));
|
||||||
const initialSort = getQueryParam('sort');
|
const initialSort = getQueryParam('sort');
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialExtensions.length > 0) {
|
if (initialExtensions.length > 0) {
|
||||||
currentFilters.extensions = initialExtensions;
|
currentFilters.extensions = initialExtensions;
|
||||||
setChoicesValues(extensionSelect, initialExtensions);
|
setChoicesValues(extensionSelect, initialExtensions);
|
||||||
@@ -151,33 +138,22 @@ export async function initInstructionsPage(): Promise<void> {
|
|||||||
document.getElementById('filter-extension')?.addEventListener('change', () => {
|
document.getElementById('filter-extension')?.addEventListener('change', () => {
|
||||||
currentFilters.extensions = getChoicesValues(extensionSelect);
|
currentFilters.extensions = getChoicesValues(extensionSelect);
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
sortSelect?.addEventListener('change', () => {
|
sortSelect?.addEventListener('change', () => {
|
||||||
currentSort = sortSelect.value as InstructionSortOption;
|
currentSort = sortSelect.value as InstructionSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
const countEl = document.getElementById('results-count');
|
|
||||||
if (countEl) {
|
|
||||||
countEl.textContent = `${allItems.length} of ${allItems.length} instructions`;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchInput?.addEventListener('input', debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200));
|
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener('click', () => {
|
clearFiltersBtn?.addEventListener('click', () => {
|
||||||
currentFilters = { extensions: [] };
|
currentFilters = { extensions: [] };
|
||||||
currentSort = 'title';
|
currentSort = 'title';
|
||||||
extensionSelect.removeActiveItems();
|
extensionSelect.removeActiveItems();
|
||||||
if (searchInput) searchInput.value = '';
|
|
||||||
if (sortSelect) sortSelect.value = 'title';
|
if (sortSelect) sortSelect.value = 'title';
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { escapeHtml, getGitHubUrl, sanitizeUrl } from '../utils';
|
import {
|
||||||
|
escapeHtml,
|
||||||
|
getGitHubUrl,
|
||||||
|
sanitizeUrl,
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
interface PluginAuthor {
|
interface PluginAuthor {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -17,6 +21,7 @@ export interface RenderablePlugin {
|
|||||||
path: string;
|
path: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
|
lastUpdated?: string | null;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
repository?: string | null;
|
repository?: string | null;
|
||||||
homepage?: string | null;
|
homepage?: string | null;
|
||||||
@@ -24,6 +29,23 @@ export interface RenderablePlugin {
|
|||||||
source?: PluginSource | null;
|
source?: PluginSource | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PluginSortOption = 'title' | 'lastUpdated';
|
||||||
|
|
||||||
|
export function sortPlugins<T extends RenderablePlugin>(
|
||||||
|
items: T[],
|
||||||
|
sort: PluginSortOption
|
||||||
|
): 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.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getExternalPluginUrl(plugin: RenderablePlugin): string {
|
function getExternalPluginUrl(plugin: RenderablePlugin): string {
|
||||||
if (plugin.source?.source === 'github' && plugin.source.repo) {
|
if (plugin.source?.source === 'github' && plugin.source.repo) {
|
||||||
const base = `https://github.com/${plugin.source.repo}`;
|
const base = `https://github.com/${plugin.source.repo}`;
|
||||||
@@ -33,20 +55,12 @@ function getExternalPluginUrl(plugin: RenderablePlugin): string {
|
|||||||
return sanitizeUrl(plugin.repository || plugin.homepage);
|
return sanitizeUrl(plugin.repository || plugin.homepage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPluginsHtml(
|
export function renderPluginsHtml(items: RenderablePlugin[]): string {
|
||||||
items: RenderablePlugin[],
|
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
|
||||||
const { query = '', highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No plugins found</h3>
|
<h3>No plugins found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>Try different tags or clear the current filters</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -64,16 +78,11 @@ export function renderPluginsHtml(
|
|||||||
const githubHref = isExternal
|
const githubHref = isExternal
|
||||||
? escapeHtml(getExternalPluginUrl(item))
|
? escapeHtml(getExternalPluginUrl(item))
|
||||||
: getGitHubUrl(item.path);
|
: getGitHubUrl(item.path);
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.name, query)
|
|
||||||
: escapeHtml(item.name);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item${isExternal ? ' resource-item-external' : ''}" data-path="${escapeHtml(item.path)}" role="listitem">
|
<article class="resource-item${isExternal ? ' resource-item-external' : ''}" data-path="${escapeHtml(item.path)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.name)}</div>
|
||||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
${metaTag}
|
${metaTag}
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ import {
|
|||||||
setChoicesValues,
|
setChoicesValues,
|
||||||
type Choices,
|
type Choices,
|
||||||
} from '../choices';
|
} from '../choices';
|
||||||
import { FuzzySearch, type SearchItem } from '../search';
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamValues,
|
getQueryParamValues,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { setupModal, openFileModal } from '../modal';
|
import { setupModal, openFileModal } from '../modal';
|
||||||
import { renderPluginsHtml, type RenderablePlugin } from './plugins-render';
|
import {
|
||||||
|
renderPluginsHtml,
|
||||||
|
sortPlugins,
|
||||||
|
type PluginSortOption,
|
||||||
|
type RenderablePlugin,
|
||||||
|
} from './plugins-render';
|
||||||
|
|
||||||
interface PluginAuthor {
|
interface PluginAuthor {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,7 +32,7 @@ interface PluginSource {
|
|||||||
path?: string;
|
path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Plugin extends SearchItem, RenderablePlugin {
|
interface Plugin extends RenderablePlugin {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -52,42 +55,44 @@ interface PluginsData {
|
|||||||
|
|
||||||
const resourceType = 'plugin';
|
const resourceType = 'plugin';
|
||||||
let allItems: Plugin[] = [];
|
let allItems: Plugin[] = [];
|
||||||
let search = new FuzzySearch<Plugin>();
|
|
||||||
let tagSelect: Choices;
|
let tagSelect: Choices;
|
||||||
|
let currentSort: PluginSortOption = 'title';
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
tags: [] as string[],
|
tags: [] as string[],
|
||||||
};
|
};
|
||||||
let resourceListHandlersReady = false;
|
let resourceListHandlersReady = false;
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function sortItems(items: Plugin[]): Plugin[] {
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
return sortPlugins(items, currentSort);
|
||||||
const countEl = document.getElementById('results-count');
|
}
|
||||||
const query = searchInput?.value || '';
|
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
function getCountText(resultsCount: number): string {
|
||||||
|
if (currentFilters.tags.length === 0) {
|
||||||
|
return `${resultsCount} plugin${resultsCount === 1 ? '' : 's'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${resultsCount} of ${allItems.length} plugins (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length === 1 ? '' : 's'})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFiltersAndRender(): void {
|
||||||
|
const countEl = document.getElementById('results-count');
|
||||||
|
let results = [...allItems];
|
||||||
|
|
||||||
if (currentFilters.tags.length > 0) {
|
if (currentFilters.tags.length > 0) {
|
||||||
results = results.filter(item => item.tags?.some(tag => currentFilters.tags.includes(tag)));
|
results = results.filter(item => item.tags?.some(tag => currentFilters.tags.includes(tag)));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItems(results, query);
|
results = sortItems(results);
|
||||||
const activeFilters: string[] = [];
|
|
||||||
if (currentFilters.tags.length > 0) activeFilters.push(`${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''}`);
|
renderItems(results);
|
||||||
let countText = `${results.length} of ${allItems.length} plugins`;
|
if (countEl) countEl.textContent = getCountText(results.length);
|
||||||
if (activeFilters.length > 0) {
|
|
||||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
|
||||||
}
|
|
||||||
if (countEl) countEl.textContent = countText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Plugin[], query = ''): void {
|
function renderItems(items: Plugin[]): void {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderPluginsHtml(items, {
|
list.innerHTML = renderPluginsHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) => search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -109,17 +114,18 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? '',
|
q: '',
|
||||||
tag: currentFilters.tags,
|
tag: currentFilters.tags,
|
||||||
|
sort: currentSort === 'title' ? '' : currentSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initPluginsPage(): Promise<void> {
|
export async function initPluginsPage(): Promise<void> {
|
||||||
const list = document.getElementById('resource-list');
|
const list = document.getElementById('resource-list');
|
||||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||||
|
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||||
|
|
||||||
setupResourceListHandlers(list as HTMLElement | null);
|
setupResourceListHandlers(list as HTMLElement | null);
|
||||||
|
|
||||||
@@ -131,21 +137,12 @@ export async function initPluginsPage(): Promise<void> {
|
|||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
|
|
||||||
// Map plugin items to search items
|
|
||||||
const searchItems = allItems.map(item => ({
|
|
||||||
...item,
|
|
||||||
title: item.name,
|
|
||||||
searchText: `${item.name} ${item.description} ${item.tags?.join(' ') || ''}`.toLowerCase()
|
|
||||||
}));
|
|
||||||
search.setItems(searchItems);
|
|
||||||
|
|
||||||
tagSelect = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
|
tagSelect = createChoices('#filter-tag', { placeholderValue: 'All Tags' });
|
||||||
tagSelect.setChoices(data.filters.tags.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
tagSelect.setChoices(data.filters.tags.map(t => ({ value: t, label: t })), 'value', 'label', true);
|
||||||
|
|
||||||
const initialQuery = getQueryParam('q');
|
|
||||||
const initialTags = getQueryParamValues('tag').filter(tag => data.filters.tags.includes(tag));
|
const initialTags = getQueryParamValues('tag').filter(tag => data.filters.tags.includes(tag));
|
||||||
|
const initialSort = getQueryParam('sort');
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialTags.length > 0) {
|
if (initialTags.length > 0) {
|
||||||
currentFilters.tags = initialTags;
|
currentFilters.tags = initialTags;
|
||||||
setChoicesValues(tagSelect, initialTags);
|
setChoicesValues(tagSelect, initialTags);
|
||||||
@@ -154,28 +151,30 @@ export async function initPluginsPage(): Promise<void> {
|
|||||||
document.getElementById('filter-tag')?.addEventListener('change', () => {
|
document.getElementById('filter-tag')?.addEventListener('change', () => {
|
||||||
currentFilters.tags = getChoicesValues(tagSelect);
|
currentFilters.tags = getChoicesValues(tagSelect);
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
const countEl = document.getElementById('results-count');
|
if (initialSort === 'lastUpdated') {
|
||||||
if (countEl) {
|
currentSort = initialSort;
|
||||||
countEl.textContent = `${allItems.length} of ${allItems.length} plugins`;
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
}
|
}
|
||||||
|
sortSelect?.addEventListener('change', () => {
|
||||||
searchInput?.addEventListener('input', debounce(() => {
|
currentSort = sortSelect.value as PluginSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
}, 200));
|
});
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener('click', () => {
|
clearFiltersBtn?.addEventListener('click', () => {
|
||||||
currentFilters = { tags: [] };
|
currentFilters = { tags: [] };
|
||||||
|
currentSort = 'title';
|
||||||
tagSelect.removeActiveItems();
|
tagSelect.removeActiveItems();
|
||||||
if (searchInput) searchInput.value = '';
|
if (sortSelect) sortSelect.value = 'title';
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
syncUrlState();
|
||||||
setupModal();
|
setupModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,45 +39,29 @@ export function sortSkills<T extends RenderableSkill>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSkillsHtml(
|
export function renderSkillsHtml(items: RenderableSkill[]): string {
|
||||||
items: RenderableSkill[],
|
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
|
||||||
const { query = "", highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No skills found</h3>
|
<h3>No skills found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>No skills are available right now.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.title, query)
|
|
||||||
: escapeHtml(item.title);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item" data-path="${escapeHtml(
|
<article class="resource-item" data-path="${escapeHtml(
|
||||||
item.skillFile
|
item.skillFile
|
||||||
)}" data-skill-id="${escapeHtml(item.id)}" role="listitem">
|
)}" data-skill-id="${escapeHtml(item.id)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.title)}</div>
|
||||||
<div class="resource-description">${escapeHtml(
|
<div class="resource-description">${escapeHtml(
|
||||||
item.description || "No description"
|
item.description || "No description"
|
||||||
)}</div>
|
)}</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
<span class="resource-tag tag-category">${escapeHtml(
|
|
||||||
item.category
|
|
||||||
)}</span>
|
|
||||||
${
|
${
|
||||||
item.hasAssets
|
item.hasAssets
|
||||||
? `<span class="resource-tag tag-assets">${
|
? `<span class="resource-tag tag-assets">${
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Skills page functionality
|
* Skills page functionality
|
||||||
*/
|
*/
|
||||||
import {
|
|
||||||
createChoices,
|
|
||||||
getChoicesValues,
|
|
||||||
setChoicesValues,
|
|
||||||
type Choices,
|
|
||||||
} from "../choices";
|
|
||||||
import { FuzzySearch, type SearchItem } from "../search";
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamFlag,
|
|
||||||
getQueryParamValues,
|
|
||||||
showToast,
|
showToast,
|
||||||
downloadZipBundle,
|
downloadZipBundle,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
@@ -33,77 +23,34 @@ interface SkillFile {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Skill extends SearchItem, Omit<RenderableSkill, "files"> {
|
interface Skill extends Omit<RenderableSkill, "files"> {
|
||||||
files: SkillFile[];
|
files: SkillFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SkillsData {
|
interface SkillsData {
|
||||||
items: Skill[];
|
items: Skill[];
|
||||||
filters: {
|
|
||||||
categories: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceType = "skill";
|
const resourceType = "skill";
|
||||||
let allItems: Skill[] = [];
|
let allItems: Skill[] = [];
|
||||||
let search = new FuzzySearch<Skill>();
|
|
||||||
let categorySelect: Choices;
|
|
||||||
let currentFilters = {
|
|
||||||
categories: [] as string[],
|
|
||||||
hasAssets: false,
|
|
||||||
};
|
|
||||||
let currentSort: SkillSortOption = "title";
|
let currentSort: SkillSortOption = "title";
|
||||||
let resourceListHandlersReady = false;
|
let resourceListHandlersReady = false;
|
||||||
|
|
||||||
function sortItems(items: Skill[]): Skill[] {
|
|
||||||
return sortSkills(items, currentSort);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById("results-count");
|
const countEl = document.getElementById("results-count");
|
||||||
const query = searchInput?.value || "";
|
const results = sortSkills(allItems, currentSort);
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
renderItems(results);
|
||||||
|
if (countEl) {
|
||||||
if (currentFilters.categories.length > 0) {
|
countEl.textContent = `${results.length} skill${results.length === 1 ? "" : "s"}`;
|
||||||
results = results.filter((item) =>
|
|
||||||
currentFilters.categories.includes(item.category)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (currentFilters.hasAssets) {
|
|
||||||
results = results.filter((item) => item.hasAssets);
|
|
||||||
}
|
|
||||||
|
|
||||||
results = sortItems(results);
|
|
||||||
|
|
||||||
renderItems(results, query);
|
|
||||||
const activeFilters: string[] = [];
|
|
||||||
if (currentFilters.categories.length > 0)
|
|
||||||
activeFilters.push(
|
|
||||||
`${currentFilters.categories.length} categor${
|
|
||||||
currentFilters.categories.length > 1 ? "ies" : "y"
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
if (currentFilters.hasAssets) activeFilters.push("has assets");
|
|
||||||
let countText = `${results.length} of ${allItems.length} skills`;
|
|
||||||
if (activeFilters.length > 0) {
|
|
||||||
countText += ` (filtered by ${activeFilters.join(", ")})`;
|
|
||||||
}
|
|
||||||
if (countEl) countEl.textContent = countText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Skill[], query = ""): void {
|
function renderItems(items: Skill[]): void {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderSkillsHtml(items, {
|
list.innerHTML = renderSkillsHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) =>
|
|
||||||
search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -142,11 +89,11 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? "",
|
q: "",
|
||||||
category: currentFilters.categories,
|
category: [],
|
||||||
hasAssets: currentFilters.hasAssets,
|
hasAssets: false,
|
||||||
sort: currentSort === "title" ? "" : currentSort,
|
sort: currentSort === "title" ? "" : currentSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -209,13 +156,6 @@ async function downloadSkill(
|
|||||||
|
|
||||||
export async function initSkillsPage(): Promise<void> {
|
export async function initSkillsPage(): Promise<void> {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const hasAssetsCheckbox = document.getElementById(
|
|
||||||
"filter-has-assets"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
|
||||||
const sortSelect = document.getElementById(
|
const sortSelect = document.getElementById(
|
||||||
"sort-select"
|
"sort-select"
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
@@ -231,76 +171,20 @@ export async function initSkillsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
categorySelect = createChoices("#filter-category", {
|
|
||||||
placeholderValue: "All Categories",
|
|
||||||
});
|
|
||||||
categorySelect.setChoices(
|
|
||||||
data.filters.categories.map((c) => ({ value: c, label: c })),
|
|
||||||
"value",
|
|
||||||
"label",
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialQuery = getQueryParam("q");
|
|
||||||
const initialCategories = getQueryParamValues("category").filter((category) =>
|
|
||||||
data.filters.categories.includes(category)
|
|
||||||
);
|
|
||||||
const initialSort = getQueryParam("sort");
|
const initialSort = getQueryParam("sort");
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialCategories.length > 0) {
|
|
||||||
currentFilters.categories = initialCategories;
|
|
||||||
setChoicesValues(categorySelect, initialCategories);
|
|
||||||
}
|
|
||||||
if (getQueryParamFlag("hasAssets")) {
|
|
||||||
currentFilters.hasAssets = true;
|
|
||||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = true;
|
|
||||||
}
|
|
||||||
if (initialSort === "lastUpdated") {
|
if (initialSort === "lastUpdated") {
|
||||||
currentSort = initialSort;
|
currentSort = initialSort;
|
||||||
if (sortSelect) sortSelect.value = initialSort;
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("filter-category")?.addEventListener("change", () => {
|
|
||||||
currentFilters.categories = getChoicesValues(categorySelect);
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
sortSelect?.addEventListener("change", () => {
|
sortSelect?.addEventListener("change", () => {
|
||||||
currentSort = sortSelect.value as SkillSortOption;
|
currentSort = sortSelect.value as SkillSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
searchInput?.addEventListener(
|
|
||||||
"input",
|
|
||||||
debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200)
|
|
||||||
);
|
|
||||||
|
|
||||||
hasAssetsCheckbox?.addEventListener("change", () => {
|
|
||||||
currentFilters.hasAssets = hasAssetsCheckbox.checked;
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener("click", () => {
|
|
||||||
currentFilters = { categories: [], hasAssets: false };
|
|
||||||
currentSort = "title";
|
|
||||||
categorySelect.removeActiveItems();
|
|
||||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
|
||||||
if (searchInput) searchInput.value = "";
|
|
||||||
if (sortSelect) sortSelect.value = "title";
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
setupModal();
|
setupModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,22 @@ export interface RenderableTool {
|
|||||||
tags: string[];
|
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 {
|
function formatMultilineText(text: string): string {
|
||||||
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
return escapeHtml(text).replace(/\r?\n/g, "<br>");
|
||||||
}
|
}
|
||||||
@@ -61,19 +77,13 @@ function getToolActionLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderToolsHtml(
|
export function renderToolsHtml(
|
||||||
tools: RenderableTool[],
|
tools: RenderableTool[]
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
): string {
|
||||||
const { query = "", highlightTitle } = options;
|
|
||||||
|
|
||||||
if (tools.length === 0) {
|
if (tools.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No tools found</h3>
|
<h3>No tools found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>Try a different category or clear the current filters</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -172,15 +182,10 @@ export function renderToolsHtml(
|
|||||||
? `<div class="tool-actions">${actions.join("")}</div>`
|
? `<div class="tool-actions">${actions.join("")}</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(tool.name, query)
|
|
||||||
: escapeHtml(tool.name);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="tool-card">
|
<div class="tool-card">
|
||||||
<div class="tool-header">
|
<div class="tool-header">
|
||||||
<h2>${titleHtml}</h2>
|
<h2>${escapeHtml(tool.name)}</h2>
|
||||||
<div class="tool-badges">
|
<div class="tool-badges">
|
||||||
${badges.join("")}
|
${badges.join("")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Tools page functionality
|
* Tools page functionality
|
||||||
*/
|
*/
|
||||||
import { FuzzySearch, type SearchableItem } from "../search";
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { renderToolsHtml } from "./tools-render";
|
import {
|
||||||
|
renderToolsHtml,
|
||||||
|
sortTools,
|
||||||
|
type ToolSortOption,
|
||||||
|
} from "./tools-render";
|
||||||
|
|
||||||
export interface Tool extends SearchableItem {
|
export interface Tool {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -46,23 +48,28 @@ interface ToolsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let allItems: Tool[] = [];
|
let allItems: Tool[] = [];
|
||||||
let search = new FuzzySearch<Tool>();
|
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
categories: [] as string[],
|
categories: [] as string[],
|
||||||
query: "",
|
|
||||||
};
|
};
|
||||||
|
let currentSort: ToolSortOption = "featured";
|
||||||
let copyHandlersReady = false;
|
let copyHandlersReady = false;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function sortItems(items: Tool[]): Tool[] {
|
||||||
const searchInput = document.getElementById(
|
return sortTools(items, currentSort);
|
||||||
"search-input"
|
}
|
||||||
) as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById("results-count");
|
|
||||||
const query = searchInput?.value || "";
|
|
||||||
currentFilters.query = query;
|
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
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) {
|
if (currentFilters.categories.length > 0) {
|
||||||
results = results.filter((item) =>
|
results = results.filter((item) =>
|
||||||
@@ -70,29 +77,23 @@ function applyFiltersAndRender(): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTools(results, query);
|
results = sortItems(results);
|
||||||
|
|
||||||
let countText = `${results.length} of ${allItems.length} tools`;
|
renderTools(results);
|
||||||
if (currentFilters.categories.length > 0) {
|
if (countEl) countEl.textContent = getCountText(results.length);
|
||||||
countText += ` (filtered by ${currentFilters.categories.length} categories)`;
|
|
||||||
}
|
|
||||||
if (countEl) countEl.textContent = countText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTools(tools: Tool[], query = ""): void {
|
function renderTools(tools: Tool[]): void {
|
||||||
const container = document.getElementById("tools-list");
|
const container = document.getElementById("tools-list");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = renderToolsHtml(tools, {
|
container.innerHTML = renderToolsHtml(tools);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) =>
|
|
||||||
search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? "",
|
q: "",
|
||||||
category: currentFilters.categories,
|
category: currentFilters.categories,
|
||||||
|
sort: currentSort === "featured" ? "" : currentSort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,13 +134,11 @@ export async function initToolsPage(): Promise<void> {
|
|||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const categoryFilter = document.getElementById(
|
const categoryFilter = document.getElementById(
|
||||||
"filter-category"
|
"filter-category"
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
|
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
|
||||||
|
|
||||||
const data = await fetchData<ToolsData>("tools.json");
|
const data = await fetchData<ToolsData>("tools.json");
|
||||||
if (!data || !data.items) {
|
if (!data || !data.items) {
|
||||||
@@ -156,9 +155,6 @@ export async function initToolsPage(): Promise<void> {
|
|||||||
title: item.name, // FuzzySearch uses title
|
title: item.name, // FuzzySearch uses title
|
||||||
}));
|
}));
|
||||||
|
|
||||||
search = new FuzzySearch<Tool>();
|
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
// Populate category filter
|
// Populate category filter
|
||||||
if (categoryFilter && data.filters.categories) {
|
if (categoryFilter && data.filters.categories) {
|
||||||
categoryFilter.innerHTML =
|
categoryFilter.innerHTML =
|
||||||
@@ -180,31 +176,32 @@ export async function initToolsPage(): Promise<void> {
|
|||||||
? [categoryFilter.value]
|
? [categoryFilter.value]
|
||||||
: [];
|
: [];
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialQuery = getQueryParam("q");
|
const initialSort = getQueryParam("sort");
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
if (initialSort === "title") {
|
||||||
|
currentSort = initialSort;
|
||||||
|
if (sortSelect) sortSelect.value = initialSort;
|
||||||
|
}
|
||||||
|
sortSelect?.addEventListener("change", () => {
|
||||||
|
currentSort = sortSelect.value as ToolSortOption;
|
||||||
|
applyFiltersAndRender();
|
||||||
|
syncUrlState();
|
||||||
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
syncUrlState();
|
||||||
// Search input handler
|
|
||||||
searchInput?.addEventListener(
|
|
||||||
"input",
|
|
||||||
debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear filters
|
// Clear filters
|
||||||
clearFiltersBtn?.addEventListener("click", () => {
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
currentFilters = { categories: [], query: "" };
|
currentFilters = { categories: [] };
|
||||||
|
currentSort = "featured";
|
||||||
if (categoryFilter) categoryFilter.value = "";
|
if (categoryFilter) categoryFilter.value = "";
|
||||||
if (searchInput) searchInput.value = "";
|
if (sortSelect) sortSelect.value = "featured";
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
setupCopyConfigHandlers();
|
setupCopyConfigHandlers();
|
||||||
|
|||||||
@@ -31,35 +31,24 @@ export function sortWorkflows<T extends RenderableWorkflow>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderWorkflowsHtml(
|
export function renderWorkflowsHtml(
|
||||||
items: RenderableWorkflow[],
|
items: RenderableWorkflow[]
|
||||||
options: {
|
|
||||||
query?: string;
|
|
||||||
highlightTitle?: (title: string, query: string) => string;
|
|
||||||
} = {}
|
|
||||||
): string {
|
): string {
|
||||||
const { query = '', highlightTitle } = options;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No workflows found</h3>
|
<h3>No workflows found</h3>
|
||||||
<p>Try a different search term or adjust filters</p>
|
<p>Try adjusting the selected filters.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const titleHtml =
|
|
||||||
query && highlightTitle
|
|
||||||
? highlightTitle(item.title, query)
|
|
||||||
: escapeHtml(item.title);
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
<article class="resource-item" data-path="${escapeHtml(item.path)}" role="listitem">
|
||||||
<button type="button" class="resource-preview">
|
<button type="button" class="resource-preview">
|
||||||
<div class="resource-info">
|
<div class="resource-info">
|
||||||
<div class="resource-title">${titleHtml}</div>
|
<div class="resource-title">${escapeHtml(item.title)}</div>
|
||||||
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
<div class="resource-description">${escapeHtml(item.description || 'No description')}</div>
|
||||||
<div class="resource-meta">
|
<div class="resource-meta">
|
||||||
${item.triggers.map((trigger) => `<span class="resource-tag tag-trigger">${escapeHtml(trigger)}</span>`).join('')}
|
${item.triggers.map((trigger) => `<span class="resource-tag tag-trigger">${escapeHtml(trigger)}</span>`).join('')}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {
|
|||||||
setChoicesValues,
|
setChoicesValues,
|
||||||
type Choices,
|
type Choices,
|
||||||
} from "../choices";
|
} from "../choices";
|
||||||
import { FuzzySearch, type SearchItem } from "../search";
|
|
||||||
import {
|
import {
|
||||||
fetchData,
|
fetchData,
|
||||||
debounce,
|
|
||||||
getQueryParam,
|
getQueryParam,
|
||||||
getQueryParamValues,
|
getQueryParamValues,
|
||||||
setupActionHandlers,
|
setupActionHandlers,
|
||||||
@@ -24,7 +22,7 @@ import {
|
|||||||
type WorkflowSortOption,
|
type WorkflowSortOption,
|
||||||
} from "./workflows-render";
|
} from "./workflows-render";
|
||||||
|
|
||||||
interface Workflow extends SearchItem, RenderableWorkflow {
|
interface Workflow extends RenderableWorkflow {
|
||||||
id: string;
|
id: string;
|
||||||
path: string;
|
path: string;
|
||||||
triggers: string[];
|
triggers: string[];
|
||||||
@@ -40,7 +38,6 @@ interface WorkflowsData {
|
|||||||
|
|
||||||
const resourceType = "workflow";
|
const resourceType = "workflow";
|
||||||
let allItems: Workflow[] = [];
|
let allItems: Workflow[] = [];
|
||||||
let search = new FuzzySearch<Workflow>();
|
|
||||||
let triggerSelect: Choices;
|
let triggerSelect: Choices;
|
||||||
let currentFilters = {
|
let currentFilters = {
|
||||||
triggers: [] as string[],
|
triggers: [] as string[],
|
||||||
@@ -53,46 +50,30 @@ function sortItems(items: Workflow[]): Workflow[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyFiltersAndRender(): void {
|
function applyFiltersAndRender(): void {
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const countEl = document.getElementById("results-count");
|
const countEl = document.getElementById("results-count");
|
||||||
const query = searchInput?.value || "";
|
let results = [...allItems];
|
||||||
|
|
||||||
let results = query ? search.search(query) : [...allItems];
|
|
||||||
|
|
||||||
if (currentFilters.triggers.length > 0) {
|
if (currentFilters.triggers.length > 0) {
|
||||||
results = results.filter((item) =>
|
results = results.filter((item) =>
|
||||||
item.triggers.some((t) => currentFilters.triggers.includes(t))
|
item.triggers.some((trigger) => currentFilters.triggers.includes(trigger))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
results = sortItems(results);
|
results = sortItems(results);
|
||||||
|
|
||||||
renderItems(results, query);
|
renderItems(results);
|
||||||
const activeFilters: string[] = [];
|
let countText = `${results.length} workflow${results.length === 1 ? "" : "s"}`;
|
||||||
if (currentFilters.triggers.length > 0)
|
if (currentFilters.triggers.length > 0) {
|
||||||
activeFilters.push(
|
countText = `${results.length} of ${allItems.length} workflows (filtered by ${currentFilters.triggers.length} trigger${currentFilters.triggers.length > 1 ? "s" : ""})`;
|
||||||
`${currentFilters.triggers.length} trigger${
|
|
||||||
currentFilters.triggers.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;
|
if (countEl) countEl.textContent = countText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items: Workflow[], query = ""): void {
|
function renderItems(items: Workflow[]): void {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
list.innerHTML = renderWorkflowsHtml(items, {
|
list.innerHTML = renderWorkflowsHtml(items);
|
||||||
query,
|
|
||||||
highlightTitle: (title, highlightQuery) =>
|
|
||||||
search.highlight(title, highlightQuery),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupResourceListHandlers(list: HTMLElement | null): void {
|
function setupResourceListHandlers(list: HTMLElement | null): void {
|
||||||
@@ -114,9 +95,9 @@ function setupResourceListHandlers(list: HTMLElement | null): void {
|
|||||||
resourceListHandlersReady = true;
|
resourceListHandlersReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUrlState(searchInput: HTMLInputElement | null): void {
|
function syncUrlState(): void {
|
||||||
updateQueryParams({
|
updateQueryParams({
|
||||||
q: searchInput?.value ?? "",
|
q: "",
|
||||||
trigger: currentFilters.triggers,
|
trigger: currentFilters.triggers,
|
||||||
sort: currentSort === "title" ? "" : currentSort,
|
sort: currentSort === "title" ? "" : currentSort,
|
||||||
});
|
});
|
||||||
@@ -124,9 +105,6 @@ function syncUrlState(searchInput: HTMLInputElement | null): void {
|
|||||||
|
|
||||||
export async function initWorkflowsPage(): Promise<void> {
|
export async function initWorkflowsPage(): Promise<void> {
|
||||||
const list = document.getElementById("resource-list");
|
const list = document.getElementById("resource-list");
|
||||||
const searchInput = document.getElementById(
|
|
||||||
"search-input"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||||
const sortSelect = document.getElementById(
|
const sortSelect = document.getElementById(
|
||||||
"sort-select"
|
"sort-select"
|
||||||
@@ -143,26 +121,22 @@ export async function initWorkflowsPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allItems = data.items;
|
allItems = data.items;
|
||||||
search.setItems(allItems);
|
|
||||||
|
|
||||||
// Setup trigger filter
|
|
||||||
triggerSelect = createChoices("#filter-trigger", {
|
triggerSelect = createChoices("#filter-trigger", {
|
||||||
placeholderValue: "All Triggers",
|
placeholderValue: "All Triggers",
|
||||||
});
|
});
|
||||||
triggerSelect.setChoices(
|
triggerSelect.setChoices(
|
||||||
data.filters.triggers.map((t) => ({ value: t, label: t })),
|
data.filters.triggers.map((trigger) => ({ value: trigger, label: trigger })),
|
||||||
"value",
|
"value",
|
||||||
"label",
|
"label",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialQuery = getQueryParam("q");
|
|
||||||
const initialTriggers = getQueryParamValues("trigger").filter((trigger) =>
|
const initialTriggers = getQueryParamValues("trigger").filter((trigger) =>
|
||||||
data.filters.triggers.includes(trigger)
|
data.filters.triggers.includes(trigger)
|
||||||
);
|
);
|
||||||
const initialSort = getQueryParam("sort");
|
const initialSort = getQueryParam("sort");
|
||||||
|
|
||||||
if (searchInput) searchInput.value = initialQuery;
|
|
||||||
if (initialTriggers.length > 0) {
|
if (initialTriggers.length > 0) {
|
||||||
currentFilters.triggers = initialTriggers;
|
currentFilters.triggers = initialTriggers;
|
||||||
setChoicesValues(triggerSelect, initialTriggers);
|
setChoicesValues(triggerSelect, initialTriggers);
|
||||||
@@ -175,36 +149,22 @@ export async function initWorkflowsPage(): Promise<void> {
|
|||||||
document.getElementById("filter-trigger")?.addEventListener("change", () => {
|
document.getElementById("filter-trigger")?.addEventListener("change", () => {
|
||||||
currentFilters.triggers = getChoicesValues(triggerSelect);
|
currentFilters.triggers = getChoicesValues(triggerSelect);
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
sortSelect?.addEventListener("change", () => {
|
sortSelect?.addEventListener("change", () => {
|
||||||
currentSort = sortSelect.value as WorkflowSortOption;
|
currentSort = sortSelect.value as WorkflowSortOption;
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
const countEl = document.getElementById("results-count");
|
|
||||||
if (countEl) {
|
|
||||||
countEl.textContent = `${allItems.length} of ${allItems.length} workflows`;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchInput?.addEventListener(
|
|
||||||
"input",
|
|
||||||
debounce(() => {
|
|
||||||
applyFiltersAndRender();
|
|
||||||
syncUrlState(searchInput);
|
|
||||||
}, 200)
|
|
||||||
);
|
|
||||||
|
|
||||||
clearFiltersBtn?.addEventListener("click", () => {
|
clearFiltersBtn?.addEventListener("click", () => {
|
||||||
currentFilters = { triggers: [] };
|
currentFilters = { triggers: [] };
|
||||||
currentSort = "title";
|
currentSort = "title";
|
||||||
triggerSelect.removeActiveItems();
|
triggerSelect.removeActiveItems();
|
||||||
if (searchInput) searchInput.value = "";
|
|
||||||
if (sortSelect) sortSelect.value = "title";
|
if (sortSelect) sortSelect.value = "title";
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
syncUrlState(searchInput);
|
syncUrlState();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyFiltersAndRender();
|
applyFiltersAndRender();
|
||||||
|
|||||||
@@ -1428,15 +1428,135 @@ body:has(#main-content) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 10px 14px;
|
padding: 0;
|
||||||
background: var(--color-glass);
|
background: transparent;
|
||||||
backdrop-filter: blur(10px);
|
border: 0;
|
||||||
border: 1px solid var(--color-glass-border);
|
border-radius: 0;
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-toolbar-row {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-toolbar .results-count {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls {
|
||||||
|
position: relative;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls[open] {
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-trigger {
|
||||||
|
list-style: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-glass);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: border-color var(--transition), background-color var(--transition), color var(--transition), box-shadow var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-trigger::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-trigger::after {
|
||||||
|
content: '▾';
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
transition: transform var(--transition), color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-trigger:hover,
|
||||||
|
.listing-controls[open] .listing-controls-trigger {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls[open] .listing-controls-trigger {
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls[open] .listing-controls-trigger::after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
right: 0;
|
||||||
|
width: min(336px, calc(100vw - 32px));
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--color-card-bg);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filters-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filter-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filters-bar .filter-group > label:not(.checkbox-label) {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filter-group select,
|
||||||
|
.listing-controls-panel .filter-group .choices {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filter-group .choices {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .choices__inner {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel .filters-bar button {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
@@ -1673,6 +1793,23 @@ body:has(#main-content) {
|
|||||||
.listing-toolbar {
|
.listing-toolbar {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-toolbar-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-trigger {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-controls-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Resource List */
|
/* Resource List */
|
||||||
|
|||||||
Reference in New Issue
Block a user