mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 03:13:25 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
---
|
||||
export type Breadcrumb = {
|
||||
paths: { name: string; href?: string }[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
const { paths, title } = Astro.props as Breadcrumb;
|
||||
---
|
||||
|
||||
<nav class="detail-breadcrumbs" aria-label="Breadcrumb">
|
||||
{
|
||||
paths.map((path, index) => (
|
||||
<>
|
||||
{path.href ? (
|
||||
<a href={path.href}>{path.name}</a>
|
||||
) : (
|
||||
<span>{path.name}</span>
|
||||
)}
|
||||
{index < paths.length - 1 && <span aria-hidden="true">/</span>}
|
||||
</>
|
||||
))
|
||||
}
|
||||
<span aria-hidden="true">/</span>
|
||||
<span aria-current="page">{title}</span>
|
||||
</nav>
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
import { getFileMeta } from "../../lib/file-types";
|
||||
|
||||
export interface BrowserFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface FileBrowserProps {
|
||||
files: BrowserFile[];
|
||||
/** Full repo path of the primary file, e.g. skills/foo/SKILL.md or hooks/foo/README.md */
|
||||
primaryPath: string;
|
||||
/** Display name of the primary file, e.g. SKILL.md or README.md */
|
||||
primaryName: string;
|
||||
/** Pre-rendered HTML for the primary file (shown by default) */
|
||||
primaryHtml: string;
|
||||
/** GitHub blob base, e.g. https://github.com/github/awesome-copilot/blob/main */
|
||||
githubBase: string;
|
||||
}
|
||||
|
||||
const { files, primaryPath, primaryName, primaryHtml, githubBase } =
|
||||
Astro.props as FileBrowserProps;
|
||||
|
||||
// Primary file first, then everything else alphabetically by name.
|
||||
const sortedFiles = [...files].sort((a, b) => {
|
||||
if (a.path === primaryPath) return -1;
|
||||
if (b.path === primaryPath) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const otherFiles = sortedFiles.filter((f) => f.path !== primaryPath);
|
||||
const rootFiles = otherFiles.filter((f) => !f.name.includes("/"));
|
||||
|
||||
// Group nested files by their top-level folder for optgroup rendering.
|
||||
const folderGroups = new Map<string, BrowserFile[]>();
|
||||
for (const file of otherFiles) {
|
||||
const slash = file.name.indexOf("/");
|
||||
if (slash === -1) continue;
|
||||
const folder = file.name.slice(0, slash);
|
||||
const bucket = folderGroups.get(folder) ?? [];
|
||||
bucket.push(file);
|
||||
folderGroups.set(folder, bucket);
|
||||
}
|
||||
const sortedFolders = [...folderGroups.keys()].sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
|
||||
const hasMultipleFiles = sortedFiles.length > 1;
|
||||
|
||||
function optionAttrs(file: BrowserFile) {
|
||||
const meta = getFileMeta(file.name);
|
||||
return {
|
||||
value: file.path,
|
||||
"data-file-name": file.name,
|
||||
"data-file-lang": meta.lang,
|
||||
"data-file-kind": meta.kind,
|
||||
};
|
||||
}
|
||||
---
|
||||
|
||||
<section
|
||||
class="skill-file-browser"
|
||||
aria-label="Bundle files"
|
||||
data-file-browser
|
||||
data-github-base={githubBase}
|
||||
data-primary-file={primaryPath}
|
||||
>
|
||||
<div class="skill-file-view">
|
||||
<header class="skill-file-view-header">
|
||||
{
|
||||
hasMultipleFiles ? (
|
||||
<div class="skill-file-picker">
|
||||
<label class="skill-file-picker-label" for="file-browser-select">
|
||||
File
|
||||
</label>
|
||||
<select
|
||||
id="file-browser-select"
|
||||
class="skill-file-select"
|
||||
data-file-select
|
||||
aria-label="Select a file to view"
|
||||
>
|
||||
<option {...optionAttrs({ name: primaryName, path: primaryPath })} selected>
|
||||
{primaryName}
|
||||
</option>
|
||||
{rootFiles.map((file) => (
|
||||
<option {...optionAttrs(file)}>{file.name}</option>
|
||||
))}
|
||||
{sortedFolders.map((folder) => (
|
||||
<optgroup label={`${folder}/`}>
|
||||
{folderGroups.get(folder)!.map((file) => (
|
||||
<option {...optionAttrs(file)}>
|
||||
{file.name.slice(folder.length + 1)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<code class="skill-file-current" data-current-file-name>
|
||||
{primaryName}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
<div class="skill-file-view-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-small"
|
||||
data-action="copy-file"
|
||||
title="Copy file contents"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
><path
|
||||
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"
|
||||
></path><path
|
||||
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
|
||||
></path></svg
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-secondary btn-small"
|
||||
data-file-github
|
||||
href={`${githubBase}/${primaryPath}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="View file on GitHub">GitHub</a
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="skill-file-content article-content"
|
||||
data-file-content
|
||||
set:html={primaryHtml}
|
||||
/>
|
||||
<div class="skill-file-status" data-file-status hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
export type HeaderItem = {
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const { item } = Astro.props as { item: HeaderItem };
|
||||
---
|
||||
|
||||
<header class="detail-header">
|
||||
<h1>{item.title}</h1>
|
||||
{item.description && <p class="detail-description">{item.description}</p>}
|
||||
</header>
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
export interface PluginIncludedItem {
|
||||
kind: string;
|
||||
path: string;
|
||||
title?: string;
|
||||
detailUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface IncludedItemsProps {
|
||||
items: PluginIncludedItem[];
|
||||
pluginPath: string;
|
||||
githubBase: string;
|
||||
}
|
||||
|
||||
const { items, pluginPath, githubBase } = Astro.props as IncludedItemsProps;
|
||||
|
||||
const KIND_LABELS: Record<string, string> = {
|
||||
agent: "Agents",
|
||||
skill: "Skills",
|
||||
instruction: "Instructions",
|
||||
hook: "Hooks",
|
||||
prompt: "Commands",
|
||||
extension: "Extensions",
|
||||
};
|
||||
|
||||
const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension"];
|
||||
|
||||
function githubHref(itemPath: string): string {
|
||||
const normalized = itemPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
||||
// Repo-root-relative paths (e.g. extensions/foo, plugins/foo) are used as-is;
|
||||
// plugin-relative paths are resolved against the plugin folder.
|
||||
const isRepoRelative = /^[a-z0-9._-]+\//i.test(normalized) &&
|
||||
(normalized.startsWith("extensions/") ||
|
||||
normalized.startsWith("plugins/") ||
|
||||
normalized.startsWith("agents/") ||
|
||||
normalized.startsWith("skills/") ||
|
||||
normalized.startsWith("instructions/") ||
|
||||
normalized.startsWith("hooks/"));
|
||||
const repoPath = isRepoRelative ? normalized : `${pluginPath}/${normalized}`;
|
||||
// GitHub uses /blob/ for files and /tree/ for directories; /tree/<ref>/<file>
|
||||
// returns 404. Detect files by a trailing extension on the last path segment.
|
||||
const lastSegment = repoPath.split("/").pop() ?? "";
|
||||
const isFile = /\.[a-z0-9]+$/i.test(lastSegment);
|
||||
const base = isFile ? githubBase.replace("/tree/", "/blob/") : githubBase;
|
||||
return `${base}/${repoPath}`;
|
||||
}
|
||||
|
||||
const grouped = KIND_ORDER.map((kind) => ({
|
||||
kind,
|
||||
label: KIND_LABELS[kind] ?? kind,
|
||||
entries: (items ?? []).filter((item) => item.kind === kind),
|
||||
})).filter((group) => group.entries.length > 0);
|
||||
---
|
||||
|
||||
{
|
||||
grouped.length > 0 && (
|
||||
<section class="detail-section included-items" aria-labelledby="included-heading">
|
||||
<h2 id="included-heading">Included items</h2>
|
||||
<p class="included-items-hint">
|
||||
This plugin bundles the following resources. Installing the plugin brings
|
||||
them all in together.
|
||||
</p>
|
||||
{grouped.map((group) => (
|
||||
<div class="included-group">
|
||||
<h3 class="included-group-title">{group.label}</h3>
|
||||
<ul class="included-list">
|
||||
{group.entries.map((item) => {
|
||||
const href = item.detailUrl ?? githubHref(item.path);
|
||||
const isExternal = !item.detailUrl;
|
||||
return (
|
||||
<li class="included-entry">
|
||||
<a
|
||||
class="included-link"
|
||||
href={href}
|
||||
{...(isExternal
|
||||
? { target: "_blank", rel: "noopener" }
|
||||
: {})}
|
||||
>
|
||||
<span class="included-kind-badge">{item.kind}</span>
|
||||
<span class="included-title">{item.title || item.path}</span>
|
||||
{isExternal && (
|
||||
<span class="included-external-hint">GitHub ↗</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
const { vscodeUrl, insidersUrl, rawMarkdown } = Astro.props as { vscodeUrl: string; insidersUrl: string; rawMarkdown?: string };
|
||||
---
|
||||
|
||||
<div class="install-dropdown" data-install-menu>
|
||||
<a
|
||||
class="btn btn-primary install-btn-main"
|
||||
href={vscodeUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"><path d="M12 5v14M5 12h14"></path></svg
|
||||
>
|
||||
Install
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary install-btn-toggle"
|
||||
aria-label="More install options"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
data-install-toggle
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
><path
|
||||
d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"
|
||||
></path></svg
|
||||
>
|
||||
</button>
|
||||
<div class="install-dropdown-menu" role="menu">
|
||||
<a href={vscodeUrl} target="_blank" rel="noopener" role="menuitem"
|
||||
>Install in VS Code</a
|
||||
>
|
||||
<a href={insidersUrl} target="_blank" rel="noopener" role="menuitem"
|
||||
>Install in VS Code Insiders</a
|
||||
>
|
||||
<div class="dropdown-divider" role="separator" aria-hidden="true"></div>
|
||||
<a href="#" role="menuitem" data-action="download">Download file</a>
|
||||
{
|
||||
rawMarkdown && (
|
||||
<a href="#" role="menuitem" data-action="copy-markdown">
|
||||
Copy markdown
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
const githubUrl = Astro.props.githubUrl as string;
|
||||
const markdownHtml = Astro.props.markdownHtml as string;
|
||||
---
|
||||
|
||||
<div class="detail-main">
|
||||
{markdownHtml ? (
|
||||
<section class="detail-section" aria-labelledby="doc-heading">
|
||||
<h2 id="doc-heading">Documentation</h2>
|
||||
<div class="article-content" set:html={markdownHtml} />
|
||||
</section>
|
||||
) : (
|
||||
<section class="detail-section">
|
||||
<p class="detail-empty">Documentation preview is unavailable. View the file <a href={githubUrl} target="_blank" rel="noopener">on GitHub</a>.</p>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
/**
|
||||
* Install actions for plugins and canvas extensions.
|
||||
*
|
||||
* Renders a split-button dropdown that deep-links into VS Code, VS Code
|
||||
* Insiders, and the GitHub Copilot app, plus an optional CLI command block.
|
||||
* The dropdown open/close is wired by resource-detail.ts via `data-install-menu`.
|
||||
*/
|
||||
export interface PluginInstallProps {
|
||||
isExternal?: boolean;
|
||||
/** awesome-copilot plugin id (internal installs). */
|
||||
pluginId?: string | null;
|
||||
/** Upstream marketplace source (owner/repo or git URL) for external installs. */
|
||||
externalSource?: string | null;
|
||||
/** CLI install command (internal only). */
|
||||
installCommand?: string | null;
|
||||
/** Heading shown above the buttons. */
|
||||
label?: string;
|
||||
/** Note shown for external installs. */
|
||||
note?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
isExternal = false,
|
||||
pluginId = null,
|
||||
externalSource = null,
|
||||
installCommand = null,
|
||||
label,
|
||||
note,
|
||||
} = Astro.props as PluginInstallProps;
|
||||
|
||||
const MARKETPLACE = "awesome-copilot";
|
||||
|
||||
let vscodeUrl: string | null = null;
|
||||
let insidersUrl: string | null = null;
|
||||
let ghappUrl: string | null = null;
|
||||
|
||||
if (isExternal) {
|
||||
if (externalSource) {
|
||||
vscodeUrl = `vscode://chat-plugin/install?source=${encodeURIComponent(
|
||||
externalSource
|
||||
)}`;
|
||||
insidersUrl = `vscode-insiders://chat-plugin/install?source=${encodeURIComponent(
|
||||
externalSource
|
||||
)}`;
|
||||
ghappUrl = `ghapp://plugins/marketplace/add?source=${encodeURIComponent(
|
||||
externalSource
|
||||
)}`;
|
||||
}
|
||||
} else if (pluginId) {
|
||||
const plugin = encodeURIComponent(pluginId);
|
||||
vscodeUrl = `vscode://chat-plugin/install?source=github/awesome-copilot&plugin=${plugin}`;
|
||||
insidersUrl = `vscode-insiders://chat-plugin/install?source=github/awesome-copilot&plugin=${plugin}`;
|
||||
ghappUrl = `ghapp://plugins/install?source=${encodeURIComponent(
|
||||
`${pluginId}@${MARKETPLACE}`
|
||||
)}`;
|
||||
}
|
||||
|
||||
const ghappLabel = isExternal
|
||||
? "Open in GitHub Copilot app"
|
||||
: "Install in GitHub Copilot app";
|
||||
|
||||
const primaryUrl = ghappUrl ?? vscodeUrl ?? insidersUrl;
|
||||
const primaryLabel =
|
||||
primaryUrl === ghappUrl
|
||||
? ghappLabel
|
||||
: primaryUrl === vscodeUrl
|
||||
? "Install in VS Code"
|
||||
: "Install in VS Code Insiders";
|
||||
const primaryIsGhapp = primaryUrl === ghappUrl;
|
||||
const heading = label ?? (isExternal ? "Install this plugin" : "Install");
|
||||
---
|
||||
|
||||
<div class="skill-install">
|
||||
<p class="skill-install-label">{heading}</p>
|
||||
{note && <p class="skill-install-note">{note}</p>}
|
||||
|
||||
{
|
||||
primaryUrl && (
|
||||
<div class="install-dropdown" data-install-menu>
|
||||
<a
|
||||
class="btn btn-primary install-btn-main"
|
||||
href={primaryUrl}
|
||||
{...(primaryIsGhapp ? {} : { target: "_blank", rel: "noopener" })}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
{primaryLabel}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary install-btn-toggle"
|
||||
aria-label="More install options"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
data-install-toggle
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="install-dropdown-menu" role="menu">
|
||||
{ghappUrl && (
|
||||
<a href={ghappUrl} role="menuitem">
|
||||
{ghappLabel}
|
||||
</a>
|
||||
)}
|
||||
{ghappUrl && (vscodeUrl || insidersUrl) && (
|
||||
<div class="dropdown-divider" role="separator" aria-hidden="true" />
|
||||
)}
|
||||
{vscodeUrl && (
|
||||
<a href={vscodeUrl} target="_blank" rel="noopener" role="menuitem">
|
||||
Install in VS Code
|
||||
</a>
|
||||
)}
|
||||
{insidersUrl && (
|
||||
<a href={insidersUrl} target="_blank" rel="noopener" role="menuitem">
|
||||
Install in VS Code Insiders
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
installCommand && (
|
||||
<>
|
||||
<p class="skill-install-label">Or install with the CLI</p>
|
||||
<div class="skill-install-command" data-install-command={installCommand}>
|
||||
<code tabindex="0">{installCommand}</code>
|
||||
<button
|
||||
type="button"
|
||||
class="skill-install-copy"
|
||||
data-action="copy-install"
|
||||
title="Copy install command"
|
||||
aria-label="Copy install command"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" />
|
||||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
const { markdown } = Astro.props as { markdown?: string };
|
||||
---
|
||||
|
||||
{
|
||||
markdown && (
|
||||
<textarea data-raw-markdown hidden aria-hidden="true" readonly set:text={markdown}></textarea>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
import type { SidebarChipsProps } from "./SidebarChips.astro";
|
||||
import SidebarChips from "./SidebarChips.astro";
|
||||
|
||||
export interface SidebarProps {
|
||||
item: any;
|
||||
lastUpdated?: string;
|
||||
frontmatterText?: string;
|
||||
githubUrl: string;
|
||||
chips?: SidebarChipsProps[];
|
||||
}
|
||||
const {
|
||||
item,
|
||||
lastUpdated,
|
||||
frontmatterText,
|
||||
githubUrl,
|
||||
chips,
|
||||
} = Astro.props as SidebarProps;
|
||||
---
|
||||
|
||||
<aside class="detail-sidebar" aria-label="Details">
|
||||
<section class="detail-card detail-actions-card" data-actions>
|
||||
<slot name="install" />
|
||||
|
||||
<div class="detail-actions-secondary">
|
||||
<button class="btn btn-secondary" type="button" data-action="share">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"
|
||||
></circle><circle cx="18" cy="19" r="3"></circle><path
|
||||
d="M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98"></path></svg
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
><path
|
||||
d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.09.68-.22.68-.48 0-.24-.01-.87-.01-1.71-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02.8-.22 1.65-.33 2.5-.33.85 0 1.7.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85 0 1.34-.01 2.42-.01 2.75 0 .27.18.58.69.48A10.01 10.01 0 0022 12c0-5.52-4.48-10-10-10z"
|
||||
></path></svg
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-card">
|
||||
<h2>Details</h2>
|
||||
<dl class="detail-meta">
|
||||
{
|
||||
chips?.map((chip) => (
|
||||
<SidebarChips
|
||||
title={chip.title}
|
||||
items={chip.items}
|
||||
filterBase={chip.filterBase}
|
||||
filterParam={chip.filterParam}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd><code class="detail-path">{item.path}</code></dd>
|
||||
</div>
|
||||
{
|
||||
lastUpdated && (
|
||||
<div>
|
||||
<dt>Last updated</dt>
|
||||
<dd>{lastUpdated}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{
|
||||
frontmatterText && (
|
||||
<section class="detail-card">
|
||||
<details class="detail-frontmatter">
|
||||
<summary>Frontmatter</summary>
|
||||
<pre>
|
||||
<code>{frontmatterText}</code>
|
||||
</pre>
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</aside>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
export type SidebarChipsProps = {
|
||||
title: string;
|
||||
items: string[];
|
||||
/**
|
||||
* When both `filterBase` and `filterParam` are provided, each chip is
|
||||
* rendered as a link to the resource's list page pre-filtered by that value
|
||||
* (e.g. `/hooks/?tag=testing`), turning read-only metadata into navigation.
|
||||
*/
|
||||
filterBase?: string;
|
||||
filterParam?: string;
|
||||
};
|
||||
|
||||
const { title, items, filterBase, filterParam } =
|
||||
Astro.props as SidebarChipsProps;
|
||||
const slug = title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
|
||||
const linkable = Boolean(filterBase && filterParam);
|
||||
const chipHref = (value: string) =>
|
||||
`${filterBase}?${filterParam}=${encodeURIComponent(value)}`;
|
||||
---
|
||||
|
||||
<div>
|
||||
<dt>{title}</dt>
|
||||
<dd>
|
||||
{
|
||||
items.length ? (
|
||||
<div class="detail-chips">
|
||||
{items.map((item) =>
|
||||
linkable ? (
|
||||
<a
|
||||
class={`resource-tag resource-tag-link tag-${slug}`}
|
||||
href={chipHref(item)}
|
||||
aria-label={`Filter by ${title.toLowerCase()}: ${item}`}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
) : (
|
||||
<span class={`resource-tag tag-${slug}`}>{item}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span class="detail-none">Not specified</span>
|
||||
)
|
||||
}
|
||||
</dd>
|
||||
</div>
|
||||
Reference in New Issue
Block a user