@grimoire-rs/indexer 0.1.8 → 0.2.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 (42) hide show
  1. package/README.md +38 -0
  2. package/dist/cli/init.js +1 -1
  3. package/dist/cli/init.js.map +1 -1
  4. package/dist/config.d.ts +21 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +19 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/enrich/index.d.ts.map +1 -1
  9. package/dist/enrich/index.js +62 -0
  10. package/dist/enrich/index.js.map +1 -1
  11. package/dist/renderer/astro/components/BrandMark.d.ts +14 -0
  12. package/dist/renderer/astro/components/BrandMark.d.ts.map +1 -0
  13. package/dist/renderer/astro/components/BrandMark.js +16 -0
  14. package/dist/renderer/astro/components/BrandMark.js.map +1 -0
  15. package/dist/renderer/astro/components/BrandMark.tsx +19 -0
  16. package/dist/renderer/astro/components/Catalog.d.ts.map +1 -1
  17. package/dist/renderer/astro/components/Catalog.js +310 -18
  18. package/dist/renderer/astro/components/Catalog.js.map +1 -1
  19. package/dist/renderer/astro/components/Catalog.tsx +445 -63
  20. package/dist/renderer/astro/components/CommandField.astro +43 -0
  21. package/dist/renderer/astro/components/PickerMenu.astro +68 -0
  22. package/dist/renderer/astro/components/VersionMenu.astro +6 -2
  23. package/dist/renderer/astro/content.config.ts +23 -5
  24. package/dist/renderer/astro/layouts/Base.astro +794 -66
  25. package/dist/renderer/astro/lib/catalog.d.ts +24 -0
  26. package/dist/renderer/astro/lib/catalog.d.ts.map +1 -1
  27. package/dist/renderer/astro/lib/catalog.js +62 -0
  28. package/dist/renderer/astro/lib/catalog.js.map +1 -1
  29. package/dist/renderer/astro/lib/catalog.ts +62 -0
  30. package/dist/renderer/astro/lib/code.d.ts +22 -0
  31. package/dist/renderer/astro/lib/code.d.ts.map +1 -0
  32. package/dist/renderer/astro/lib/code.js +21 -0
  33. package/dist/renderer/astro/lib/code.js.map +1 -0
  34. package/dist/renderer/astro/lib/code.ts +21 -0
  35. package/dist/renderer/astro/pages/index.astro +144 -36
  36. package/dist/renderer/astro/pages/p/[...slug].astro +403 -55
  37. package/dist/renderer/index.d.ts +23 -0
  38. package/dist/renderer/index.d.ts.map +1 -1
  39. package/dist/renderer/index.js +117 -30
  40. package/dist/renderer/index.js.map +1 -1
  41. package/package.json +5 -1
  42. package/dist/renderer/astro/components/CopyButton.astro +0 -24
@@ -2,13 +2,31 @@
2
2
  // Frozen public URL: /p/<namespace>/<name>/. grim resolves against it, the
3
3
  // VS Code deep link points at it, and search engines index it — the route
4
4
  // shape is a contract, not an implementation detail.
5
+ import { ChevronRight, FolderRoot, Globe, Image, ImageOff, Scale, User } from "lucide-preact";
5
6
  import Base from "../../layouts/Base.astro";
6
7
  import VersionMenu from "../../components/VersionMenu.astro";
7
- import CopyButton from "../../components/CopyButton.astro";
8
+ import CommandField from "../../components/CommandField.astro";
9
+ import PickerMenu from "../../components/PickerMenu.astro";
10
+ import { BrandMark } from "../../components/BrandMark.tsx";
11
+ import {
12
+ mdiBitbucket,
13
+ mdiGit,
14
+ mdiGithub,
15
+ mdiGitlab,
16
+ mdiMicrosoftVisualStudioCode,
17
+ } from "@mdi/js";
8
18
  import { withBase } from "../../lib/base";
9
- import { compareVersions, versionCascade, vscodeUrl, type CatalogPackage } from "../../lib/catalog";
19
+ import { SHIKI_THEMES } from "../../lib/code";
20
+ import {
21
+ compareVersions,
22
+ resolveMemberRef,
23
+ versionCascade,
24
+ vscodeUrl,
25
+ type CatalogPackage,
26
+ } from "../../lib/catalog";
10
27
  import { data } from "../../lib/data";
11
28
  import { getEntry, render } from "astro:content";
29
+ import { Code } from "astro:components";
12
30
 
13
31
  export function getStaticPaths() {
14
32
  return data.packages.map((pkg) => ({
@@ -17,7 +35,7 @@ export function getStaticPaths() {
17
35
  }));
18
36
  }
19
37
 
20
- const { config } = data;
38
+ const { config, packages } = data;
21
39
  const { pkg } = Astro.props as { pkg: CatalogPackage };
22
40
  const id = `${pkg.namespace}/${pkg.name}`;
23
41
 
@@ -26,14 +44,72 @@ const changelogEntry = await getEntry("changelogs", id);
26
44
  const Readme = readmeEntry ? (await render(readmeEntry)).Content : null;
27
45
  const Changelog = changelogEntry ? (await render(changelogEntry)).Content : null;
28
46
 
47
+ // What the artifact actually is, as opposed to what its README says about
48
+ // itself. Three shapes, one per kind family: a skill, rule or agent carries
49
+ // its entry-point markdown; a bundle carries the list it installs; an MCP
50
+ // server carries its descriptor. `enrich` writes whichever applies.
51
+ const contentsEntry = await getEntry("contents", id);
52
+ const Contents = contentsEntry ? (await render(contentsEntry)).Content : null;
53
+ const contentsData = (await getEntry("contentData", id))?.data as
54
+ | { members?: { kind?: string; name?: string; id?: string }[] }
55
+ | undefined;
56
+
57
+ // Members resolved against this index: a member published elsewhere has no
58
+ // page here, so it is named without a link rather than linked into a 404.
59
+ const memberList = contentsData?.members ?? [];
60
+ const members = memberList.map((member) => {
61
+ const ref = member.id ? resolveMemberRef(pkg.ref, member.id) : null;
62
+ const target = ref ? packages.find((p) => p.ref === ref) : undefined;
63
+ return {
64
+ kind: member.kind ?? "",
65
+ name: member.name ?? ref ?? "",
66
+ ref,
67
+ href: target ? withBase(`/p/${target.namespace}/${target.name}/`) : null,
68
+ };
69
+ });
70
+
29
71
  const tags = [...(pkg.tags ?? [])].sort(compareVersions);
30
72
  const cascade = versionCascade(pkg.version, tags);
31
73
  const older = tags.filter((t) => !cascade.includes(t));
74
+ // Highlight what an untagged `grim add` actually installs. That is `latest`
75
+ // wherever it was published — the exact release is one of the things latest
76
+ // currently points at, not the default.
77
+ const defaultTag = cascade.includes("latest") ? "latest" : (pkg.version ?? cascade[0]);
78
+
79
+ // The forge the repository link points at, drawn as its own mark. `mdiGit`
80
+ // is the honest fallback for a host with no brand in the set — it still says
81
+ // "source repository", where a generic branch glyph said nothing.
82
+ const FORGES = [
83
+ { match: /(^|\.)github\.com$/i, path: mdiGithub },
84
+ { match: /(^|\.)gitlab\.[a-z.]+$/i, path: mdiGitlab },
85
+ { match: /(^|\.)bitbucket\.org$/i, path: mdiBitbucket },
86
+ ];
87
+ const repoHost = URL.canParse(pkg.repository ?? "") ? new URL(pkg.repository!).hostname : "";
88
+ const repoMark = FORGES.find((f) => f.match.test(repoHost))?.path ?? mdiGit;
89
+
90
+ // Which tabs have anything behind them. An empty one is offered but not
91
+ // selectable — greyed in the ribbon rather than clickable through to a
92
+ // sentence apologising for itself. The first tab that does have content is
93
+ // the one that opens, so a package with no README does not land on a blank.
94
+ const filled = {
95
+ readme: Readme !== null,
96
+ contents: Contents !== null || members.length > 0 || contentsData !== undefined,
97
+ changelog: Changelog !== null,
98
+ };
99
+ const openTab = (["readme", "contents", "changelog"] as const).find((tab) => filled[tab]) ?? null;
32
100
 
33
101
  const title = pkg.title ?? pkg.name;
34
102
  const description = pkg.summary ?? pkg.description ?? `${pkg.kind} package: ${pkg.ref}`;
35
103
  const vscode = vscodeUrl(config.vscodeExtension, pkg.ref);
36
104
  const addCommand = `grim add ${pkg.ref}`;
105
+
106
+ // The same two-way scope choice the package cards offer and the hero's
107
+ // registry bar draws, with the same two glyphs and the same order — global
108
+ // first, because it needs no project open.
109
+ const scopeChoices = [
110
+ { name: "Global", command: `grim add --global ${pkg.ref}`, Icon: Globe },
111
+ { name: "Project", command: addCommand, Icon: FolderRoot },
112
+ ];
37
113
  ---
38
114
 
39
115
  <Base title={`${title} — ${config.brand}`} description={description} image={pkg.logo}>
@@ -41,7 +117,18 @@ const addCommand = `grim add ${pkg.ref}`;
41
117
  <header class="detail-head">
42
118
  {
43
119
  pkg.logo ? (
44
- <img class="tile logo" src={withBase(pkg.logo)} alt="" />
120
+ // The placeholder ships in the markup and the image fades in over
121
+ // it, so a slow or failing load never flashes a broken glyph. The
122
+ // slot starts `loading`; the handler in `Base.astro` moves it to
123
+ // `ready` or `broken`. Icon mirrors `LogoMark` in `Catalog.tsx`.
124
+ <span class="tile logo-slot" data-static data-state="loading">
125
+ {/* Both marks ship; CSS shows one per state. The card grid picks
126
+ between them in Preact instead, but this page has to settle
127
+ its placeholder before any script runs. */}
128
+ <Image class="logo-mark mark-loading" aria-hidden="true" />
129
+ <ImageOff class="logo-mark mark-broken" aria-hidden="true" />
130
+ <img src={withBase(pkg.logo)} alt="" />
131
+ </span>
45
132
  ) : (
46
133
  <div class="tile letter" style={`background: var(--kind-${pkg.kind}, var(--muted))`}>
47
134
  {pkg.name[0]?.toUpperCase()}
@@ -64,22 +151,120 @@ const addCommand = `grim add ${pkg.ref}`;
64
151
  )
65
152
  }
66
153
 
67
- <div class="install-row">
68
- <code>{addCommand}</code>
69
- <CopyButton value={addCommand} label="Copy the install command" />
154
+ <div class="cmd-bar" data-os-switch data-os-noun={`add command for ${pkg.name}`}>
155
+ <PickerMenu choices={scopeChoices} title="Scope" label="Choose the scope" />
156
+ <CommandField
157
+ value={scopeChoices[0].command}
158
+ name={`${scopeChoices[0].name} add command for ${pkg.name}`}
159
+ label="Copy the add command"
160
+ />
161
+ {
162
+ vscode && (
163
+ <a
164
+ class="seg brand-link"
165
+ href={vscode}
166
+ title="Open in VS Code"
167
+ aria-label={`Open ${pkg.name} in VS Code`}
168
+ >
169
+ <BrandMark path={mdiMicrosoftVisualStudioCode} />
170
+ </a>
171
+ )
172
+ }
70
173
  </div>
71
174
 
72
175
  <div class="detail-body">
73
176
  <main class="detail-main">
74
- {Readme ? <Readme /> : <p class="empty">No README available.</p>}
177
+ {/* Radio tabs, not a scripted tablist: the ribbon works on a page
178
+ whose JS never ran, and a radio group already has the keyboard
179
+ behaviour tabs want (arrows move, only one selected). The inputs
180
+ are visually hidden rather than `hidden`, which would take them
181
+ out of the focus order along with the pixels. */}
182
+ {/* All three tabs are always offered — a ribbon that grows a tab per
183
+ package makes the panel a different shape on every page. An empty
184
+ one is disabled instead: greyed, not clickable through to a
185
+ sentence apologising for itself. */}
186
+ <input
187
+ type="radio"
188
+ name="panel"
189
+ id="panel-readme"
190
+ class="sr-only"
191
+ checked={openTab === "readme"}
192
+ disabled={!filled.readme}
193
+ />
194
+ <input
195
+ type="radio"
196
+ name="panel"
197
+ id="panel-contents"
198
+ class="sr-only"
199
+ checked={openTab === "contents"}
200
+ disabled={!filled.contents}
201
+ />
202
+ <input
203
+ type="radio"
204
+ name="panel"
205
+ id="panel-changelog"
206
+ class="sr-only"
207
+ checked={openTab === "changelog"}
208
+ disabled={!filled.changelog}
209
+ />
210
+ {/* The greyed tab says it is not for clicking; the tooltip says why,
211
+ so the answer is one hover away instead of a panel of its own. */}
212
+ <div class="ribbon">
213
+ <label for="panel-readme" title={filled.readme ? undefined : "no readme available"}>
214
+ Readme
215
+ </label>
216
+ <label for="panel-contents" title={filled.contents ? undefined : "no contents available"}>
217
+ Contents
218
+ </label>
219
+ <label
220
+ for="panel-changelog"
221
+ title={filled.changelog ? undefined : "no changelog available"}
222
+ >
223
+ Changelog
224
+ </label>
225
+ </div>
226
+ {filled.readme && (
227
+ <div class="prose tab-panel" id="tab-readme">
228
+ <Readme />
229
+ </div>
230
+ )}
75
231
  {
76
- Changelog && (
77
- <details class="changelog">
78
- <summary>Changelog</summary>
79
- <Changelog />
80
- </details>
232
+ filled.contents && (
233
+ <div class="prose tab-panel" id="tab-contents">
234
+ {Contents ? (
235
+ <Contents />
236
+ ) : members.length > 0 ? (
237
+ <ul class="members">
238
+ {members.map((member) => (
239
+ <li>
240
+ {member.href ? (
241
+ <a href={member.href}>{member.name}</a>
242
+ ) : (
243
+ <span>{member.name}</span>
244
+ )}
245
+ <span class={`badge kind-${member.kind}`}>{member.kind}</span>
246
+ <code>{member.ref}</code>
247
+ </li>
248
+ ))}
249
+ </ul>
250
+ ) : (
251
+ <Code
252
+ code={JSON.stringify(contentsData, null, 2)}
253
+ lang="json"
254
+ themes={SHIKI_THEMES}
255
+ />
256
+ )}
257
+ </div>
81
258
  )
82
259
  }
260
+ {filled.changelog && (
261
+ <div class="prose tab-panel" id="tab-changelog">
262
+ <Changelog />
263
+ </div>
264
+ )}
265
+ {/* Nothing anywhere: one statement in the middle of the panel, rather
266
+ than three tabs each denying it separately. */}
267
+ {openTab === null && <p class="panel-empty">No content available.</p>}
83
268
  </main>
84
269
 
85
270
  <aside class="detail-rail">
@@ -88,12 +273,15 @@ const addCommand = `grim add ${pkg.ref}`;
88
273
  <h2>Versions</h2>
89
274
  <div class="cascade">
90
275
  {cascade.map((tag) => (
91
- <VersionMenu pkgRef={pkg.ref} tag={tag} current={tag === pkg.version} />
276
+ <VersionMenu pkgRef={pkg.ref} tag={tag} current={tag === defaultTag} />
92
277
  ))}
93
278
  </div>
94
279
  {older.length > 0 && (
95
280
  <details class="older">
96
- <summary>{older.length} older</summary>
281
+ <summary>
282
+ <ChevronRight size={14} aria-hidden="true" />
283
+ older versions
284
+ </summary>
97
285
  <div class="cascade">
98
286
  {older.map((tag) => (
99
287
  <VersionMenu pkgRef={pkg.ref} tag={tag} />
@@ -109,13 +297,19 @@ const addCommand = `grim add ${pkg.ref}`;
109
297
  <dl>
110
298
  {pkg.license && (
111
299
  <>
112
- <dt>License</dt>
300
+ <dt>
301
+ <Scale size={15} aria-hidden="true" />
302
+ License
303
+ </dt>
113
304
  <dd>{pkg.license}</dd>
114
305
  </>
115
306
  )}
116
307
  {pkg.repository && (
117
308
  <>
118
- <dt>Repository</dt>
309
+ <dt>
310
+ <BrandMark path={repoMark} size={15} />
311
+ Repository
312
+ </dt>
119
313
  <dd>
120
314
  <a href={pkg.repository} target="_blank" rel="noopener noreferrer">
121
315
  source
@@ -125,7 +319,10 @@ const addCommand = `grim add ${pkg.ref}`;
125
319
  )}
126
320
  {pkg.owner?.github && (
127
321
  <>
128
- <dt>Owner</dt>
322
+ <dt>
323
+ <User size={15} aria-hidden="true" />
324
+ Owner
325
+ </dt>
129
326
  <dd>
130
327
  <a
131
328
  href={`https://github.com/${pkg.owner.github}`}
@@ -141,15 +338,19 @@ const addCommand = `grim add ${pkg.ref}`;
141
338
  {pkg.keywords && pkg.keywords.length > 0 && (
142
339
  <ul class="keywords">
143
340
  {pkg.keywords.map((k) => (
144
- <li>{k}</li>
341
+ <li>
342
+ {/* Back to the catalog with the term already in the search
343
+ box — a keyword is only useful if it leads somewhere. */}
344
+ <a
345
+ href={withBase(`/?q=${encodeURIComponent(k)}`)}
346
+ title={`Find packages tagged ${k}`}
347
+ >
348
+ {k}
349
+ </a>
350
+ </li>
145
351
  ))}
146
352
  </ul>
147
353
  )}
148
- {vscode && (
149
- <a class="vscode-link" href={vscode}>
150
- Open in VS Code
151
- </a>
152
- )}
153
354
  </section>
154
355
  </aside>
155
356
  </div>
@@ -247,20 +448,6 @@ const addCommand = `grim add ${pkg.ref}`;
247
448
  padding: 0.6rem 0.9rem;
248
449
  font-weight: 600;
249
450
  }
250
- .install-row {
251
- display: flex;
252
- align-items: center;
253
- justify-content: space-between;
254
- gap: 0.6rem;
255
- background: var(--card);
256
- border: 1px solid var(--border);
257
- border-radius: 8px;
258
- padding: 0.6rem 0.9rem;
259
- }
260
- .install-row code {
261
- font-size: 0.9rem;
262
- overflow-x: auto;
263
- }
264
451
  .detail-body {
265
452
  display: grid;
266
453
  grid-template-columns: 1fr 18rem;
@@ -279,20 +466,122 @@ const addCommand = `grim add ${pkg.ref}`;
279
466
  border-radius: 10px;
280
467
  padding: 1.25rem 1.5rem;
281
468
  }
469
+ /* Markdown opens with its own heading, whose margin lands on top of the
470
+ panel padding and reads as a hole. Every panel sets its own top edge. */
471
+ .prose > :global(:first-child) {
472
+ margin-block-start: 0;
473
+ }
282
474
  .detail-main :global(img) {
283
475
  max-width: 100%;
284
476
  }
285
477
  .detail-main :global(pre) {
286
478
  overflow-x: auto;
287
479
  }
288
- .changelog {
289
- margin-top: 1.5rem;
290
- border-top: 1px solid var(--border);
291
- padding-top: 1rem;
480
+ /* Full-bleed inside the panel's padding, so the rule under it reads as the
481
+ panel's own top edge rather than a line floating in the content. */
482
+ .ribbon {
483
+ display: flex;
484
+ gap: 1.25rem;
485
+ margin: -1.25rem -1.5rem 1.25rem;
486
+ padding: 0 1.5rem;
487
+ border-bottom: 1px solid var(--border);
292
488
  }
293
- .changelog summary {
489
+ .ribbon label {
490
+ padding: 0.7rem 0;
491
+ margin-bottom: -1px; /* the active underline sits on the panel rule */
492
+ border-bottom: 2px solid transparent;
493
+ font-size: 0.85rem;
494
+ text-transform: uppercase;
495
+ letter-spacing: 0.04em;
496
+ color: var(--muted);
294
497
  cursor: pointer;
295
- font-weight: 600;
498
+ }
499
+ .ribbon label:hover {
500
+ color: var(--fg);
501
+ }
502
+ /* Nothing behind it. Still shown, so the ribbon is the same shape on every
503
+ package page, but visibly not a thing to click. */
504
+ #panel-readme:disabled ~ .ribbon label[for="panel-readme"],
505
+ #panel-contents:disabled ~ .ribbon label[for="panel-contents"],
506
+ #panel-changelog:disabled ~ .ribbon label[for="panel-changelog"] {
507
+ opacity: 0.4;
508
+ cursor: default;
509
+ }
510
+ #panel-readme:disabled ~ .ribbon label[for="panel-readme"]:hover,
511
+ #panel-contents:disabled ~ .ribbon label[for="panel-contents"]:hover,
512
+ #panel-changelog:disabled ~ .ribbon label[for="panel-changelog"]:hover {
513
+ color: var(--muted);
514
+ }
515
+ /* Every tab empty: say it once, in the middle. */
516
+ .panel-empty {
517
+ display: grid;
518
+ place-items: center;
519
+ min-height: 11rem;
520
+ margin: 0;
521
+ color: var(--muted);
522
+ }
523
+ .tab-panel {
524
+ display: none;
525
+ }
526
+ #panel-readme:checked ~ #tab-readme,
527
+ #panel-contents:checked ~ #tab-contents,
528
+ #panel-changelog:checked ~ #tab-changelog {
529
+ display: block;
530
+ }
531
+ #panel-readme:checked ~ .ribbon label[for="panel-readme"],
532
+ #panel-contents:checked ~ .ribbon label[for="panel-contents"],
533
+ #panel-changelog:checked ~ .ribbon label[for="panel-changelog"] {
534
+ color: var(--fg);
535
+ border-bottom-color: var(--accent);
536
+ }
537
+ /* The input carries the focus, the label shows it. */
538
+ #panel-readme:focus-visible ~ .ribbon label[for="panel-readme"],
539
+ #panel-contents:focus-visible ~ .ribbon label[for="panel-contents"],
540
+ #panel-changelog:focus-visible ~ .ribbon label[for="panel-changelog"] {
541
+ outline: 2px solid var(--accent);
542
+ outline-offset: 2px;
543
+ border-radius: 3px;
544
+ }
545
+
546
+ /* A bundle's member list: what it installs, one row each. A grid, not a
547
+ row of flex items — names and badges of different widths have to line up
548
+ down the column, which is the whole reason to list them. `subgrid` puts
549
+ each <li>'s three cells on the parent's tracks. */
550
+ .members {
551
+ list-style: none;
552
+ margin: 0;
553
+ padding: 0;
554
+ display: grid;
555
+ grid-template-columns: auto auto 1fr;
556
+ gap: 0.45rem 0.75rem;
557
+ align-items: center;
558
+ }
559
+ .members li {
560
+ display: grid;
561
+ grid-column: 1 / -1;
562
+ grid-template-columns: subgrid;
563
+ align-items: center;
564
+ }
565
+ /* A grid item stretches by default, so a short badge grew to the width of
566
+ the longest one in the column and its pill outran its own text. */
567
+ .members .badge {
568
+ justify-self: start;
569
+ }
570
+ .members code {
571
+ color: var(--muted);
572
+ font-size: 0.82rem;
573
+ overflow-wrap: anywhere;
574
+ }
575
+ /* Narrow: three columns of a long ref do not fit, so each member becomes
576
+ its own block instead of a squeezed row. */
577
+ @media (max-width: 34rem) {
578
+ .members,
579
+ .members li {
580
+ grid-template-columns: auto 1fr;
581
+ }
582
+ .members li > code {
583
+ grid-column: 1 / -1;
584
+ }
296
585
  }
297
586
  .detail-rail {
298
587
  display: flex;
@@ -305,6 +594,8 @@ const addCommand = `grim add ${pkg.ref}`;
305
594
  border-radius: 10px;
306
595
  padding: 1rem;
307
596
  }
597
+ /* Panel label. The main panel's ribbon labels match this by hand — they
598
+ are <label>s, not headings, so they cannot join the selector. */
308
599
  .detail-rail h2 {
309
600
  margin: 0 0 0.6rem;
310
601
  font-size: 0.85rem;
@@ -320,31 +611,69 @@ const addCommand = `grim add ${pkg.ref}`;
320
611
  gap: 0.3rem;
321
612
  }
322
613
  .older {
323
- margin-top: 0.5rem;
324
- }
325
- .older[open] > summary {
326
- margin-bottom: 0.5rem;
614
+ margin-top: 0.7rem;
327
615
  }
616
+ /* The UA marker is a glyph that flips between two characters with no
617
+ in-between; a real chevron can turn. `list-style` kills it everywhere
618
+ but WebKit, which needs its own pseudo-element. Closed points right at
619
+ the row it would open, open points down at it — a quarter turn, the way
620
+ a file tree behaves. */
328
621
  .older > summary {
622
+ display: flex;
623
+ align-items: center;
624
+ gap: 0.35rem;
625
+ list-style: none;
626
+ padding-block: 0.15rem;
329
627
  cursor: pointer;
330
628
  font-size: 0.8rem;
331
629
  color: var(--muted);
332
630
  }
631
+ .older > summary::-webkit-details-marker {
632
+ display: none;
633
+ }
333
634
  .older > summary:hover {
334
635
  color: var(--fg);
335
636
  }
637
+ .older > summary :global(svg) {
638
+ flex-shrink: 0;
639
+ transition: transform 160ms ease;
640
+ }
641
+ .older[open] > summary :global(svg) {
642
+ transform: rotate(90deg);
643
+ }
644
+ @media (prefers-reduced-motion: reduce) {
645
+ .older > summary :global(svg) {
646
+ transition: none;
647
+ }
648
+ }
649
+ .older[open] > summary {
650
+ margin-bottom: 0.5rem;
651
+ }
336
652
  dl {
337
653
  margin: 0;
338
654
  display: grid;
339
655
  grid-template-columns: auto 1fr;
340
- gap: 0.3rem 0.6rem;
656
+ /* A row pairs a small muted label with a larger value. Grid's default
657
+ `stretch` sits each at the top of its own box, so the size difference
658
+ reads as two lines that never quite line up. */
659
+ align-items: center;
660
+ gap: 0.45rem 0.7rem;
341
661
  }
342
662
  dt {
663
+ display: flex;
664
+ align-items: center;
665
+ gap: 0.4rem;
343
666
  color: var(--muted);
344
667
  font-size: 0.85rem;
345
668
  }
669
+ /* Every icon is the same box, so the label text lands on one x without an
670
+ icon column of its own. */
671
+ dt :global(svg) {
672
+ flex-shrink: 0;
673
+ }
346
674
  dd {
347
675
  margin: 0;
676
+ font-size: 0.9rem;
348
677
  }
349
678
  .keywords {
350
679
  list-style: none;
@@ -354,15 +683,34 @@ const addCommand = `grim add ${pkg.ref}`;
354
683
  flex-wrap: wrap;
355
684
  gap: 0.35rem;
356
685
  }
357
- .keywords li {
358
- background: var(--chip-bg);
686
+ /* The chip is the link, not a box around it — otherwise the padding is
687
+ dead space the pointer falls through. Bordered and hovering to accent
688
+ like the version pills beside it and the kind chips on the catalog:
689
+ every other pill on the site answers a pointer the same way, and one
690
+ that answers differently reads as a different kind of thing. */
691
+ .keywords a {
692
+ display: inline-block;
693
+ border: 1px solid var(--border);
359
694
  border-radius: 999px;
695
+ background: var(--chip-bg);
360
696
  padding: 0.15rem 0.6rem;
361
697
  font-size: 0.78rem;
698
+ color: var(--muted);
699
+ text-decoration: none;
700
+ transition:
701
+ color 120ms ease,
702
+ border-color 120ms ease,
703
+ background-color 120ms ease;
362
704
  }
363
- .vscode-link {
364
- display: inline-block;
365
- margin-top: 1rem;
366
- font-size: 0.9rem;
705
+ .keywords a:hover,
706
+ .keywords a:focus-visible {
707
+ color: var(--accent);
708
+ border-color: var(--accent);
709
+ background: var(--accent-soft);
710
+ }
711
+ @media (prefers-reduced-motion: reduce) {
712
+ .keywords a {
713
+ transition: none;
714
+ }
367
715
  }
368
716
  </style>
@@ -6,6 +6,20 @@ export interface BuildSiteOptions {
6
6
  outDir: string;
7
7
  config: SiteConfig;
8
8
  }
9
+ export interface DevSiteOptions extends BuildSiteOptions {
10
+ /**
11
+ * Astro `srcDir`. Point it at the working tree's `src/renderer/astro` to
12
+ * get HMR on the templates being edited; omitted, dev stages a copy and
13
+ * behaves like a build that never finishes.
14
+ */
15
+ srcDir?: string;
16
+ port?: number;
17
+ }
18
+ /** A running dev server. `stop()` also removes the staged directory. */
19
+ export interface DevServer {
20
+ url: string;
21
+ stop(): Promise<void>;
22
+ }
9
23
  /** Thrown when `<outDir>/all.json` is missing or malformed. */
10
24
  export declare class RenderInputError extends Error {
11
25
  constructor(message: string);
@@ -16,5 +30,14 @@ export declare const ASTRO_SRC_DIR: string;
16
30
  export declare const DEFAULT_PUBLIC_DIR: string;
17
31
  /** Render the catalog site from `<outDir>/all.json` into `outDir`. */
18
32
  export declare function buildSite(opts: BuildSiteOptions): Promise<void>;
33
+ /**
34
+ * Serve the same site `buildSite` would emit, with HMR — the review loop for
35
+ * a rendering change, so the look can be judged without cutting a release.
36
+ *
37
+ * The staged dir and the chdir both outlive the call, because the server
38
+ * does: Vite resolves the staged `node_modules` on every request. `stop()`
39
+ * undoes both.
40
+ */
41
+ export declare function devSite(opts: DevSiteOptions): Promise<DevServer>;
19
42
  export type { CatalogPackage, GrimoireData } from "./types.js";
20
43
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,+DAA+D;AAC/D,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AA4BD,mEAAmE;AACnE,eAAO,MAAM,aAAa,QAA2B,CAAC;AACtD,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAkH5D,sEAAsE;AACtE,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CrE;AAED,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAI9D,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,+DAA+D;AAC/D,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AA4BD,mEAAmE;AACnE,eAAO,MAAM,aAAa,QAA2B,CAAC;AACtD,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAuM5D,sEAAsE;AACtE,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrE;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CA2BtE;AAED,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}