@grimoire-rs/indexer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/cli/build.d.ts +6 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +29 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/exit.d.ts +28 -0
- package/dist/cli/exit.d.ts.map +1 -0
- package/dist/cli/exit.js +33 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +40 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +317 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +112 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/validate.d.ts +18 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +139 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +134 -0
- package/dist/config.js.map +1 -0
- package/dist/data/index.d.ts +40 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +103 -0
- package/dist/data/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +2 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +4 -0
- package/dist/integration.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.d.ts +6 -0
- package/dist/renderer/astro/components/Catalog.d.ts.map +1 -0
- package/dist/renderer/astro/components/Catalog.js +73 -0
- package/dist/renderer/astro/components/Catalog.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.tsx +256 -0
- package/dist/renderer/astro/components/CopyButton.astro +24 -0
- package/dist/renderer/astro/components/VersionMenu.astro +105 -0
- package/dist/renderer/astro/content.config.ts +30 -0
- package/dist/renderer/astro/layouts/Base.astro +623 -0
- package/dist/renderer/astro/lib/catalog.d.ts +7 -0
- package/dist/renderer/astro/lib/catalog.d.ts.map +1 -0
- package/dist/renderer/astro/lib/catalog.js +61 -0
- package/dist/renderer/astro/lib/catalog.js.map +1 -0
- package/dist/renderer/astro/lib/catalog.ts +69 -0
- package/dist/renderer/astro/lib/data.d.ts +3 -0
- package/dist/renderer/astro/lib/data.d.ts.map +1 -0
- package/dist/renderer/astro/lib/data.js +4 -0
- package/dist/renderer/astro/lib/data.js.map +1 -0
- package/dist/renderer/astro/lib/data.ts +12 -0
- package/dist/renderer/astro/pages/index.astro +69 -0
- package/dist/renderer/astro/pages/p/[...slug].astro +367 -0
- package/dist/renderer/index.d.ts +20 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +192 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/public/favicon.svg +4 -0
- package/dist/renderer/types.d.ts +34 -0
- package/dist/renderer/types.d.ts.map +1 -0
- package/dist/renderer/types.js +4 -0
- package/dist/renderer/types.js.map +1 -0
- package/dist/validate/adapters/files.d.ts +17 -0
- package/dist/validate/adapters/files.d.ts.map +1 -0
- package/dist/validate/adapters/files.js +50 -0
- package/dist/validate/adapters/files.js.map +1 -0
- package/dist/validate/adapters/forge.d.ts +25 -0
- package/dist/validate/adapters/forge.d.ts.map +1 -0
- package/dist/validate/adapters/forge.js +100 -0
- package/dist/validate/adapters/forge.js.map +1 -0
- package/dist/validate/adapters/http.d.ts +24 -0
- package/dist/validate/adapters/http.d.ts.map +1 -0
- package/dist/validate/adapters/http.js +75 -0
- package/dist/validate/adapters/http.js.map +1 -0
- package/dist/validate/adapters/registry.d.ts +16 -0
- package/dist/validate/adapters/registry.d.ts.map +1 -0
- package/dist/validate/adapters/registry.js +91 -0
- package/dist/validate/adapters/registry.js.map +1 -0
- package/dist/validate/core/metadata.d.ts +67 -0
- package/dist/validate/core/metadata.d.ts.map +1 -0
- package/dist/validate/core/metadata.js +149 -0
- package/dist/validate/core/metadata.js.map +1 -0
- package/dist/validate/core/ownership.d.ts +43 -0
- package/dist/validate/core/ownership.d.ts.map +1 -0
- package/dist/validate/core/ownership.js +33 -0
- package/dist/validate/core/ownership.js.map +1 -0
- package/dist/validate/core/paths.d.ts +21 -0
- package/dist/validate/core/paths.d.ts.map +1 -0
- package/dist/validate/core/paths.js +34 -0
- package/dist/validate/core/paths.js.map +1 -0
- package/dist/validate/index.d.ts +40 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +206 -0
- package/dist/validate/index.js.map +1 -0
- package/package.json +50 -0
- package/templates/README.md +48 -0
- package/templates/github-pages.yml +24 -0
- package/templates/github-validate.yml +36 -0
- package/templates/gitignore +9 -0
- package/templates/gitlab-ci.yml +13 -0
- package/templates/publish.toml +17 -0
- package/templates/reusable/gitlab-index-ci.yml +61 -0
- package/templates/reusable/index-pages.yml +80 -0
- package/templates/reusable/index-validate.yml +88 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useMemo, useState } from "preact/hooks";
|
|
2
|
+
import { timeAgo, vscodeUrl, type CatalogPackage } from "../lib/catalog.js";
|
|
3
|
+
|
|
4
|
+
// Known kinds get stable chip ordering + badge colors; unknown kinds
|
|
5
|
+
// (future schema growth) still render with a neutral badge.
|
|
6
|
+
const KNOWN_KINDS = ["skill", "rule", "agent", "mcp", "bundle"];
|
|
7
|
+
|
|
8
|
+
function kindOrder(kind: string): number {
|
|
9
|
+
const i = KNOWN_KINDS.indexOf(kind);
|
|
10
|
+
return i === -1 ? KNOWN_KINDS.length : i;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Sort = "name" | "updated";
|
|
14
|
+
|
|
15
|
+
// Deprecated packages sink to the bottom regardless of sort mode; the
|
|
16
|
+
// chosen sort only orders within the two groups.
|
|
17
|
+
function compare(a: CatalogPackage, b: CatalogPackage, sort: Sort): number {
|
|
18
|
+
const dep = Number(!!a.deprecated) - Number(!!b.deprecated);
|
|
19
|
+
if (dep !== 0) return dep;
|
|
20
|
+
if (sort === "name") return a.name.localeCompare(b.name);
|
|
21
|
+
if (!a.created || !b.created) return (a.created ? 0 : 1) - (b.created ? 0 : 1);
|
|
22
|
+
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The two install scopes wear the same glyphs as the VS Code extension, so
|
|
26
|
+
// "project" and "global" read identically in both: codicons `root-folder`
|
|
27
|
+
// and `globe` (@vscode/codicons 0.0.45, CC-BY-4.0), copied as paths rather
|
|
28
|
+
// than pulling in the icon font for two icons.
|
|
29
|
+
const SCOPE_PROJECT =
|
|
30
|
+
"M4.5 7C2.019 7 0 9.019 0 11.5C0 13.981 2.019 16 4.5 16C6.981 16 9 13.981 9 11.5C9 9.019 6.981 7 4.5 7ZM4.5 15C2.57 15 1 13.43 1 11.5C1 9.57 2.57 8 4.5 8C6.43 8 8 9.57 8 11.5C8 13.43 6.43 15 4.5 15ZM7 11.5C7 12.881 5.881 14 4.5 14C3.119 14 2 12.881 2 11.5C2 10.119 3.119 9 4.5 9C5.881 9 7 10.119 7 11.5ZM15 6.5V11.5C15 12.881 13.881 14 12.5 14H10V13H12.5C13.328 13 14 12.328 14 11.5V6.5C14 5.672 13.328 5 12.5 5H8.207L7.207 6H5.586C5.719 6 5.846 5.947 5.94 5.854L7.294 4.5L5.94 3.146C5.846 3.052 5.719 3 5.586 3H3.5C2.672 3 2 3.672 2 4.5V6H1V4.5C1 3.119 2.119 2 3.5 2H5.586C5.984 2 6.365 2.158 6.647 2.439L8.208 4H12.501C13.882 4 15.001 5.119 15.001 6.5H15Z";
|
|
31
|
+
const SCOPE_GLOBAL =
|
|
32
|
+
"M8 1C4.141 1 1 4.141 1 8C1 11.859 4.141 15 8 15C11.859 15 15 11.859 15 8C15 4.141 11.859 1 8 1ZM8 14C7.422 14 6.686 12.906 6.288 11H9.713C9.315 12.906 8.579 14 8.001 14H8ZM6.121 10C6.044 9.392 6 8.723 6 8C6 7.277 6.044 6.608 6.121 6H9.878C9.955 6.608 9.999 7.277 9.999 8C9.999 8.723 9.955 9.392 9.878 10H6.121ZM2 8C2 7.299 2.121 6.626 2.343 6H5.121C5.041 6.656 5 7.332 5 8C5 8.668 5.041 9.344 5.121 10H2.343C2.121 9.374 2 8.701 2 8ZM8 2C8.578 2 9.314 3.094 9.712 5H6.287C6.685 3.094 7.422 2 8 2ZM10.879 6H13.657C13.879 6.626 14 7.299 14 8C14 8.701 13.879 9.374 13.657 10H10.879C10.959 9.344 11 8.668 11 8C11 7.332 10.959 6.656 10.879 6ZM13.195 5H10.722C10.516 3.938 10.199 2.98 9.775 2.268C11.228 2.719 12.446 3.707 13.195 5ZM6.226 2.268C5.802 2.98 5.484 3.938 5.279 5H2.806C3.556 3.707 4.774 2.718 6.226 2.268ZM2.805 11H5.278C5.484 12.062 5.801 13.02 6.225 13.732C4.772 13.281 3.554 12.293 2.805 11ZM9.774 13.732C10.198 13.02 10.516 12.062 10.721 11H13.194C12.444 12.293 11.226 13.282 9.774 13.732Z";
|
|
33
|
+
const CHECK =
|
|
34
|
+
"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 1 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z";
|
|
35
|
+
|
|
36
|
+
function CopyButton({
|
|
37
|
+
command,
|
|
38
|
+
variant = "default",
|
|
39
|
+
}: {
|
|
40
|
+
command: string;
|
|
41
|
+
variant?: "default" | "global";
|
|
42
|
+
}) {
|
|
43
|
+
const [copied, setCopied] = useState(false);
|
|
44
|
+
const copy = () => {
|
|
45
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
46
|
+
setCopied(true);
|
|
47
|
+
setTimeout(() => setCopied(false), 1500);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
class={copied ? "copy copied" : "copy"}
|
|
54
|
+
title={command}
|
|
55
|
+
aria-label={`Copy: ${command}`}
|
|
56
|
+
onClick={copy}
|
|
57
|
+
>
|
|
58
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
|
|
59
|
+
<path d={copied ? CHECK : variant === "global" ? SCOPE_GLOBAL : SCOPE_PROJECT} />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `vscodeExtension` arrives as a prop, not from lib/data: this island
|
|
66
|
+
// hydrates in the browser, so importing the build-time payload here would
|
|
67
|
+
// ship the whole catalog twice.
|
|
68
|
+
export default function Catalog({
|
|
69
|
+
packages,
|
|
70
|
+
vscodeExtension,
|
|
71
|
+
}: {
|
|
72
|
+
packages: CatalogPackage[];
|
|
73
|
+
vscodeExtension: string | null;
|
|
74
|
+
}) {
|
|
75
|
+
const [query, setQuery] = useState("");
|
|
76
|
+
const [kind, setKind] = useState<string | null>(null);
|
|
77
|
+
const [sort, setSort] = useState<Sort>("name");
|
|
78
|
+
|
|
79
|
+
const kinds = useMemo(() => {
|
|
80
|
+
const counts = new Map<string, number>();
|
|
81
|
+
for (const p of packages) counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
|
|
82
|
+
return [...counts.entries()].sort(
|
|
83
|
+
(a, b) => kindOrder(a[0]) - kindOrder(b[0]) || a[0].localeCompare(b[0]),
|
|
84
|
+
);
|
|
85
|
+
}, [packages]);
|
|
86
|
+
|
|
87
|
+
const q = query.trim().toLowerCase();
|
|
88
|
+
const shown = packages
|
|
89
|
+
.filter((p) => {
|
|
90
|
+
if (kind && p.kind !== kind) return false;
|
|
91
|
+
if (!q) return true;
|
|
92
|
+
return [
|
|
93
|
+
p.name,
|
|
94
|
+
p.description ?? "",
|
|
95
|
+
p.namespace,
|
|
96
|
+
p.kind,
|
|
97
|
+
p.ref,
|
|
98
|
+
p.summary ?? "",
|
|
99
|
+
(p.keywords ?? []).join(" "),
|
|
100
|
+
].some((field) => field.toLowerCase().includes(q));
|
|
101
|
+
})
|
|
102
|
+
.sort((a, b) => compare(a, b, sort));
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<section>
|
|
106
|
+
<div class="controls">
|
|
107
|
+
<input
|
|
108
|
+
type="search"
|
|
109
|
+
placeholder={`Search ${packages.length} packages…`}
|
|
110
|
+
value={query}
|
|
111
|
+
onInput={(e) => setQuery((e.target as HTMLInputElement).value)}
|
|
112
|
+
aria-label="Search packages"
|
|
113
|
+
/>
|
|
114
|
+
<div class="chips" role="group" aria-label="Sort by">
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class={sort === "name" ? "chip active" : "chip"}
|
|
118
|
+
onClick={() => setSort("name")}
|
|
119
|
+
>
|
|
120
|
+
name
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
class={sort === "updated" ? "chip active" : "chip"}
|
|
125
|
+
onClick={() => setSort("updated")}
|
|
126
|
+
>
|
|
127
|
+
updated
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="chips" role="group" aria-label="Filter by kind">
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class={kind === null ? "chip active" : "chip"}
|
|
134
|
+
onClick={() => setKind(null)}
|
|
135
|
+
>
|
|
136
|
+
all <small>{packages.length}</small>
|
|
137
|
+
</button>
|
|
138
|
+
{kinds.map(([k, count]) => (
|
|
139
|
+
<button
|
|
140
|
+
key={k}
|
|
141
|
+
type="button"
|
|
142
|
+
class={kind === k ? `chip active kind-${k}` : `chip kind-${k}`}
|
|
143
|
+
onClick={() => setKind(kind === k ? null : k)}
|
|
144
|
+
>
|
|
145
|
+
{k} <small>{count}</small>
|
|
146
|
+
</button>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{shown.length === 0 ? (
|
|
152
|
+
<p class="empty">No packages match.</p>
|
|
153
|
+
) : (
|
|
154
|
+
<ul class="grid">
|
|
155
|
+
{shown.map((p) => (
|
|
156
|
+
<li key={`${p.namespace}/${p.name}`} class="card">
|
|
157
|
+
<div class="card-head">
|
|
158
|
+
{p.logo ? (
|
|
159
|
+
<img class="card-logo" src={p.logo} alt="" loading="lazy" />
|
|
160
|
+
) : (
|
|
161
|
+
<span
|
|
162
|
+
class="card-logo card-logo-fallback"
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
style={{ background: `var(--kind-${p.kind}, var(--muted))` }}
|
|
165
|
+
>
|
|
166
|
+
{p.name[0]?.toUpperCase()}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
<h2>
|
|
170
|
+
<a href={`/p/${p.namespace}/${p.name}/`}>{p.name}</a>
|
|
171
|
+
</h2>
|
|
172
|
+
{p.deprecated ? (
|
|
173
|
+
<span class="badge deprecated">deprecated</span>
|
|
174
|
+
) : (
|
|
175
|
+
<span class={`badge kind-${p.kind}`}>{p.kind}</span>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
<p class="namespace">{p.namespace}</p>
|
|
179
|
+
{(p.version || p.license || p.created) && (
|
|
180
|
+
<div class="meta-row">
|
|
181
|
+
{p.version && <span class="pill version">v{p.version}</span>}
|
|
182
|
+
{p.license && <span class="pill license">{p.license}</span>}
|
|
183
|
+
{p.created && timeAgo(p.created) && (
|
|
184
|
+
<time class="updated" datetime={p.created} title={p.created}>
|
|
185
|
+
updated {timeAgo(p.created)}
|
|
186
|
+
</time>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
{p.deprecated && (
|
|
191
|
+
<p class="deprecated-strip">
|
|
192
|
+
deprecated
|
|
193
|
+
{p.replacedBy ? ` — replaced by ${p.replacedBy}` : ""}
|
|
194
|
+
</p>
|
|
195
|
+
)}
|
|
196
|
+
{p.description && <p class="description">{p.description}</p>}
|
|
197
|
+
{p.keywords && p.keywords.length > 0 && (
|
|
198
|
+
<div class="keywords">
|
|
199
|
+
{p.keywords.slice(0, 5).map((kw) => (
|
|
200
|
+
<button
|
|
201
|
+
key={kw}
|
|
202
|
+
type="button"
|
|
203
|
+
class="chip keyword"
|
|
204
|
+
onClick={() => setQuery(kw)}
|
|
205
|
+
>
|
|
206
|
+
{kw}
|
|
207
|
+
</button>
|
|
208
|
+
))}
|
|
209
|
+
{p.keywords.length > 5 && (
|
|
210
|
+
<span class="chip keyword overflow">
|
|
211
|
+
+{p.keywords.length - 5}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<div class="card-foot">
|
|
217
|
+
<div class="copy-group">
|
|
218
|
+
<CopyButton command={`grim add ${p.ref}`} />
|
|
219
|
+
<CopyButton
|
|
220
|
+
command={`grim add --global ${p.ref}`}
|
|
221
|
+
variant="global"
|
|
222
|
+
/>
|
|
223
|
+
{vscodeUrl(vscodeExtension, p.ref) && (
|
|
224
|
+
<a
|
|
225
|
+
class="copy vscode"
|
|
226
|
+
href={vscodeUrl(vscodeExtension, p.ref)!}
|
|
227
|
+
title="Open in VS Code"
|
|
228
|
+
aria-label={`Open ${p.name} in VS Code`}
|
|
229
|
+
>
|
|
230
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
231
|
+
<path
|
|
232
|
+
fill="currentColor"
|
|
233
|
+
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12l-3.573 3.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
|
234
|
+
/>
|
|
235
|
+
</svg>
|
|
236
|
+
</a>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
{p.repository && (
|
|
240
|
+
<a
|
|
241
|
+
class="source"
|
|
242
|
+
href={p.repository}
|
|
243
|
+
target="_blank"
|
|
244
|
+
rel="noopener noreferrer"
|
|
245
|
+
>
|
|
246
|
+
source
|
|
247
|
+
</a>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
</li>
|
|
251
|
+
))}
|
|
252
|
+
</ul>
|
|
253
|
+
)}
|
|
254
|
+
</section>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Labelled copy button. The click handler is the shared `[data-copy]`
|
|
3
|
+
// listener in Base.astro — this component only renders the markup that
|
|
4
|
+
// listener expects (a `<span>` label it can swap to "copied").
|
|
5
|
+
interface Props {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
const { value, label } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<button type="button" class="copy" data-copy={value} aria-label={label}>
|
|
13
|
+
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
|
14
|
+
<path
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
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"
|
|
17
|
+
></path>
|
|
18
|
+
<path
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
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"
|
|
21
|
+
></path>
|
|
22
|
+
</svg>
|
|
23
|
+
<span>copy</span>
|
|
24
|
+
</button>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
// One version segment. Left-click copies the pinned identifier; right-click
|
|
3
|
+
// opens the grim commands for it, at the pointer. The menu is a native
|
|
4
|
+
// popover for the top layer (nothing can clip it, no z-index fights), but
|
|
5
|
+
// `manual`, not `auto`: an auto popover light-dismisses on the mouseup of
|
|
6
|
+
// the very right-click that opened it, so the menu flashes and vanishes.
|
|
7
|
+
// The page script therefore owns placement AND dismissal.
|
|
8
|
+
interface Props {
|
|
9
|
+
pkgRef: string;
|
|
10
|
+
tag: string;
|
|
11
|
+
current?: boolean;
|
|
12
|
+
}
|
|
13
|
+
const { pkgRef, tag, current = false } = Astro.props;
|
|
14
|
+
const pinned = `${pkgRef}:${tag}`;
|
|
15
|
+
const menuId = `vm-${tag.replace(/[^a-z0-9]+/gi, "-")}`;
|
|
16
|
+
const commands: [string, string][] = [
|
|
17
|
+
["copy reference", pinned],
|
|
18
|
+
["add to project", `grim add ${pinned}`],
|
|
19
|
+
["add globally", `grim add --global ${pinned}`],
|
|
20
|
+
["describe", `grim describe ${pinned}`],
|
|
21
|
+
["fetch", `grim fetch ${pinned}`],
|
|
22
|
+
];
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class:list={["version", { current }]}
|
|
28
|
+
data-copy={pinned}
|
|
29
|
+
data-menu={menuId}
|
|
30
|
+
aria-current={current ? "true" : undefined}
|
|
31
|
+
title={`Copy ${pinned} — right-click for commands`}>{tag}</button
|
|
32
|
+
>
|
|
33
|
+
|
|
34
|
+
<div popover="manual" id={menuId} class="version-menu">
|
|
35
|
+
{
|
|
36
|
+
commands.map(([label, cmd]) => (
|
|
37
|
+
<button type="button" class="cmd" data-copy={cmd} title={cmd}>
|
|
38
|
+
<span>{label}</span>
|
|
39
|
+
</button>
|
|
40
|
+
))
|
|
41
|
+
}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
/* No `span` inside: the shared copy script only swaps a label when it
|
|
46
|
+
finds one, so the tag keeps its text and flashes the class instead. */
|
|
47
|
+
.version {
|
|
48
|
+
border: 1px solid var(--border);
|
|
49
|
+
border-radius: 999px;
|
|
50
|
+
background: transparent;
|
|
51
|
+
padding: 0.15rem 0.6rem;
|
|
52
|
+
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
53
|
+
font-size: 0.85rem;
|
|
54
|
+
color: var(--muted);
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
.version:hover,
|
|
59
|
+
.version:focus-visible {
|
|
60
|
+
color: var(--accent);
|
|
61
|
+
border-color: var(--accent);
|
|
62
|
+
background: var(--accent-soft);
|
|
63
|
+
}
|
|
64
|
+
.version.current {
|
|
65
|
+
color: var(--fg);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
border-color: var(--accent);
|
|
68
|
+
}
|
|
69
|
+
.version.copied {
|
|
70
|
+
color: var(--accent);
|
|
71
|
+
background: var(--accent-soft);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.version-menu {
|
|
75
|
+
/* Overrides the UA's centred `inset: 0; margin: auto` — the page script
|
|
76
|
+
sets left/top from the pointer. */
|
|
77
|
+
position: fixed;
|
|
78
|
+
inset: auto;
|
|
79
|
+
margin: 0;
|
|
80
|
+
min-width: 11rem;
|
|
81
|
+
padding: 0.3rem;
|
|
82
|
+
background: var(--card);
|
|
83
|
+
border: 1px solid var(--border);
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.15);
|
|
86
|
+
}
|
|
87
|
+
.cmd {
|
|
88
|
+
display: block;
|
|
89
|
+
width: 100%;
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: 5px;
|
|
92
|
+
background: transparent;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-size: 0.82rem;
|
|
96
|
+
text-align: left;
|
|
97
|
+
padding: 0.3rem 0.5rem;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
.cmd:hover,
|
|
101
|
+
.cmd:focus-visible {
|
|
102
|
+
background: var(--chip-bg);
|
|
103
|
+
color: var(--fg);
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
|
|
4
|
+
// Loads enrich/<namespace>/<name>/{readme,changelog}.md. `base` is
|
|
5
|
+
// resolved relative to the Astro root, which `buildSite` sets to the index
|
|
6
|
+
// repo root — no cwd walk-up, no marker file. The tree is optional:
|
|
7
|
+
// glob() degrades to an empty collection when its base dir doesn't exist.
|
|
8
|
+
import { defineCollection } from "astro:content";
|
|
9
|
+
import { glob } from "astro/loaders";
|
|
10
|
+
|
|
11
|
+
// id = "<namespace>/<name>", matching the page slug, by stripping the
|
|
12
|
+
// trailing "/<file>.md" off the glob-relative entry path.
|
|
13
|
+
function idFromFile(file: string) {
|
|
14
|
+
const re = new RegExp(`/${file}\\.md$`);
|
|
15
|
+
return ({ entry }: { entry: string }) => entry.replace(re, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const readmes = defineCollection({
|
|
19
|
+
loader: glob({ pattern: "**/readme.md", base: "./enrich", generateId: idFromFile("readme") }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const changelogs = defineCollection({
|
|
23
|
+
loader: glob({
|
|
24
|
+
pattern: "**/changelog.md",
|
|
25
|
+
base: "./enrich",
|
|
26
|
+
generateId: idFromFile("changelog"),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const collections = { readmes, changelogs };
|