@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.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/cli/build.d.ts +6 -0
  4. package/dist/cli/build.d.ts.map +1 -0
  5. package/dist/cli/build.js +29 -0
  6. package/dist/cli/build.js.map +1 -0
  7. package/dist/cli/exit.d.ts +28 -0
  8. package/dist/cli/exit.d.ts.map +1 -0
  9. package/dist/cli/exit.js +33 -0
  10. package/dist/cli/exit.js.map +1 -0
  11. package/dist/cli/index.d.ts +3 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +8 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/cli/init.d.ts +40 -0
  16. package/dist/cli/init.d.ts.map +1 -0
  17. package/dist/cli/init.js +317 -0
  18. package/dist/cli/init.js.map +1 -0
  19. package/dist/cli/main.d.ts +2 -0
  20. package/dist/cli/main.d.ts.map +1 -0
  21. package/dist/cli/main.js +112 -0
  22. package/dist/cli/main.js.map +1 -0
  23. package/dist/cli/validate.d.ts +18 -0
  24. package/dist/cli/validate.d.ts.map +1 -0
  25. package/dist/cli/validate.js +139 -0
  26. package/dist/cli/validate.js.map +1 -0
  27. package/dist/config.d.ts +76 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +134 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/data/index.d.ts +40 -0
  32. package/dist/data/index.d.ts.map +1 -0
  33. package/dist/data/index.js +103 -0
  34. package/dist/data/index.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +4 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integration.d.ts +2 -0
  40. package/dist/integration.d.ts.map +1 -0
  41. package/dist/integration.js +4 -0
  42. package/dist/integration.js.map +1 -0
  43. package/dist/renderer/astro/components/Catalog.d.ts +6 -0
  44. package/dist/renderer/astro/components/Catalog.d.ts.map +1 -0
  45. package/dist/renderer/astro/components/Catalog.js +73 -0
  46. package/dist/renderer/astro/components/Catalog.js.map +1 -0
  47. package/dist/renderer/astro/components/Catalog.tsx +256 -0
  48. package/dist/renderer/astro/components/CopyButton.astro +24 -0
  49. package/dist/renderer/astro/components/VersionMenu.astro +105 -0
  50. package/dist/renderer/astro/content.config.ts +30 -0
  51. package/dist/renderer/astro/layouts/Base.astro +623 -0
  52. package/dist/renderer/astro/lib/catalog.d.ts +7 -0
  53. package/dist/renderer/astro/lib/catalog.d.ts.map +1 -0
  54. package/dist/renderer/astro/lib/catalog.js +61 -0
  55. package/dist/renderer/astro/lib/catalog.js.map +1 -0
  56. package/dist/renderer/astro/lib/catalog.ts +69 -0
  57. package/dist/renderer/astro/lib/data.d.ts +3 -0
  58. package/dist/renderer/astro/lib/data.d.ts.map +1 -0
  59. package/dist/renderer/astro/lib/data.js +4 -0
  60. package/dist/renderer/astro/lib/data.js.map +1 -0
  61. package/dist/renderer/astro/lib/data.ts +12 -0
  62. package/dist/renderer/astro/pages/index.astro +69 -0
  63. package/dist/renderer/astro/pages/p/[...slug].astro +367 -0
  64. package/dist/renderer/index.d.ts +20 -0
  65. package/dist/renderer/index.d.ts.map +1 -0
  66. package/dist/renderer/index.js +192 -0
  67. package/dist/renderer/index.js.map +1 -0
  68. package/dist/renderer/public/favicon.svg +4 -0
  69. package/dist/renderer/types.d.ts +34 -0
  70. package/dist/renderer/types.d.ts.map +1 -0
  71. package/dist/renderer/types.js +4 -0
  72. package/dist/renderer/types.js.map +1 -0
  73. package/dist/validate/adapters/files.d.ts +17 -0
  74. package/dist/validate/adapters/files.d.ts.map +1 -0
  75. package/dist/validate/adapters/files.js +50 -0
  76. package/dist/validate/adapters/files.js.map +1 -0
  77. package/dist/validate/adapters/forge.d.ts +25 -0
  78. package/dist/validate/adapters/forge.d.ts.map +1 -0
  79. package/dist/validate/adapters/forge.js +100 -0
  80. package/dist/validate/adapters/forge.js.map +1 -0
  81. package/dist/validate/adapters/http.d.ts +24 -0
  82. package/dist/validate/adapters/http.d.ts.map +1 -0
  83. package/dist/validate/adapters/http.js +75 -0
  84. package/dist/validate/adapters/http.js.map +1 -0
  85. package/dist/validate/adapters/registry.d.ts +16 -0
  86. package/dist/validate/adapters/registry.d.ts.map +1 -0
  87. package/dist/validate/adapters/registry.js +91 -0
  88. package/dist/validate/adapters/registry.js.map +1 -0
  89. package/dist/validate/core/metadata.d.ts +67 -0
  90. package/dist/validate/core/metadata.d.ts.map +1 -0
  91. package/dist/validate/core/metadata.js +149 -0
  92. package/dist/validate/core/metadata.js.map +1 -0
  93. package/dist/validate/core/ownership.d.ts +43 -0
  94. package/dist/validate/core/ownership.d.ts.map +1 -0
  95. package/dist/validate/core/ownership.js +33 -0
  96. package/dist/validate/core/ownership.js.map +1 -0
  97. package/dist/validate/core/paths.d.ts +21 -0
  98. package/dist/validate/core/paths.d.ts.map +1 -0
  99. package/dist/validate/core/paths.js +34 -0
  100. package/dist/validate/core/paths.js.map +1 -0
  101. package/dist/validate/index.d.ts +40 -0
  102. package/dist/validate/index.d.ts.map +1 -0
  103. package/dist/validate/index.js +206 -0
  104. package/dist/validate/index.js.map +1 -0
  105. package/package.json +50 -0
  106. package/templates/README.md +48 -0
  107. package/templates/github-pages.yml +24 -0
  108. package/templates/github-validate.yml +36 -0
  109. package/templates/gitignore +9 -0
  110. package/templates/gitlab-ci.yml +13 -0
  111. package/templates/publish.toml +17 -0
  112. package/templates/reusable/gitlab-index-ci.yml +61 -0
  113. package/templates/reusable/index-pages.yml +80 -0
  114. 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 };