@grimoire-rs/indexer 0.4.3 → 0.5.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 (61) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/NOTICE +30 -0
  3. package/README.md +76 -331
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +35 -4
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/config.d.ts +107 -7
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +181 -36
  10. package/dist/config.js.map +1 -1
  11. package/dist/renderer/astro/components/CardLogo.d.ts +5 -0
  12. package/dist/renderer/astro/components/CardLogo.js +58 -0
  13. package/dist/renderer/astro/components/CardLogo.tsx +96 -0
  14. package/dist/renderer/astro/components/Catalog.d.ts +14 -1
  15. package/dist/renderer/astro/components/Catalog.js +467 -108
  16. package/dist/renderer/astro/components/Catalog.tsx +756 -349
  17. package/dist/renderer/astro/components/CommandBar.astro +66 -0
  18. package/dist/renderer/astro/components/CopyButton.d.ts +7 -0
  19. package/dist/renderer/astro/components/CopyButton.js +28 -0
  20. package/dist/renderer/astro/components/CopyButton.tsx +56 -0
  21. package/dist/renderer/astro/components/KindMark.d.ts +69 -0
  22. package/dist/renderer/astro/components/KindMark.js +66 -0
  23. package/dist/renderer/astro/components/KindMark.tsx +141 -0
  24. package/dist/renderer/astro/components/PackageCard.d.ts +18 -0
  25. package/dist/renderer/astro/components/PackageCard.js +50 -0
  26. package/dist/renderer/astro/components/PackageCard.tsx +273 -0
  27. package/dist/renderer/astro/components/PackageRow.d.ts +10 -0
  28. package/dist/renderer/astro/components/PackageRow.js +32 -0
  29. package/dist/renderer/astro/components/PackageRow.tsx +126 -0
  30. package/dist/renderer/astro/components/PickerMenu.astro +5 -14
  31. package/dist/renderer/astro/components/SiteFooter.astro +64 -0
  32. package/dist/renderer/astro/components/SiteHeader.astro +74 -0
  33. package/dist/renderer/astro/components/VersionMenu.astro +2 -2
  34. package/dist/renderer/astro/layouts/Base.astro +860 -206
  35. package/dist/renderer/astro/lib/base.d.ts +25 -0
  36. package/dist/renderer/astro/lib/base.js +23 -0
  37. package/dist/renderer/astro/lib/base.ts +27 -0
  38. package/dist/renderer/astro/lib/catalog.d.ts +24 -0
  39. package/dist/renderer/astro/lib/catalog.js +36 -0
  40. package/dist/renderer/astro/lib/catalog.ts +37 -0
  41. package/dist/renderer/astro/lib/commands.d.ts +58 -0
  42. package/dist/renderer/astro/lib/commands.js +86 -0
  43. package/dist/renderer/astro/lib/commands.ts +117 -0
  44. package/dist/renderer/astro/lib/keywordRail.d.ts +44 -0
  45. package/dist/renderer/astro/lib/keywordRail.js +99 -0
  46. package/dist/renderer/astro/lib/keywordRail.ts +110 -0
  47. package/dist/renderer/astro/pages/index.astro +40 -87
  48. package/dist/renderer/astro/pages/p/[...slug].astro +340 -195
  49. package/dist/renderer/astro/styles/tokens.css +40 -5
  50. package/dist/renderer/index.d.ts +58 -0
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +547 -5
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/renderer/types.d.ts +9 -0
  55. package/dist/renderer/types.d.ts.map +1 -1
  56. package/package.json +9 -4
  57. package/templates/README.md +6 -0
  58. package/templates/ci/github-ratings.yml +5 -0
  59. package/templates/gitignore +4 -1
  60. package/templates/theme/README.md +38 -0
  61. package/templates/tsconfig.json +47 -0
@@ -3,11 +3,10 @@
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
5
  import {
6
+ ArrowBigUp,
6
7
  BookOpen,
7
8
  Bug,
8
9
  ChevronRight,
9
- FolderRoot,
10
- Globe,
11
10
  History,
12
11
  House,
13
12
  Image,
@@ -16,13 +15,14 @@ import {
16
15
  MessageCircle,
17
16
  Scale,
18
17
  ShieldAlert,
18
+ TriangleAlert,
19
19
  User,
20
20
  } from "lucide-preact";
21
21
  import Base from "../../layouts/Base.astro";
22
22
  import VersionMenu from "../../components/VersionMenu.astro";
23
- import CommandField from "../../components/CommandField.astro";
24
- import PickerMenu from "../../components/PickerMenu.astro";
23
+ import CommandBar from "../../components/CommandBar.astro";
25
24
  import { BrandMark } from "../../components/BrandMark.tsx";
25
+ import { DEPRECATED_MARK, KIND_MARKS, KindMark } from "../../components/KindMark.tsx";
26
26
  import {
27
27
  mdiBitbucket,
28
28
  mdiGit,
@@ -34,14 +34,17 @@ import { withBase } from "../../lib/base";
34
34
  import { SHIKI_THEMES } from "../../lib/code";
35
35
  import {
36
36
  compareVersions,
37
+ deprecationNote,
37
38
  externalUrl,
38
39
  lastUpdated,
39
40
  resolveMemberRef,
40
41
  timeAgo,
41
42
  versionCascade,
42
43
  vscodeUrl,
44
+ vscodeVoteUrl,
43
45
  type CatalogPackage,
44
46
  } from "../../lib/catalog";
47
+ import { addArtifactChoices } from "../../lib/commands";
45
48
  import { data } from "../../lib/data";
46
49
  import { getEntry, render } from "astro:content";
47
50
  import { Code } from "astro:components";
@@ -122,12 +125,8 @@ const vscode = vscodeUrl(config.vscodeExtension, pkg.ref);
122
125
  const addCommand = `grim add ${pkg.ref}`;
123
126
 
124
127
  // The same two-way scope choice the package cards offer and the hero's
125
- // registry bar draws, with the same two glyphs and the same order — global
126
- // first, because it needs no project open.
127
- const scopeChoices = [
128
- { name: "Global", command: `grim add --global ${pkg.ref}`, Icon: Globe },
129
- { name: "Project", command: addCommand, Icon: FolderRoot },
130
- ];
128
+ // registry bar draws, from the one helper all three read.
129
+ const scopeChoices = addArtifactChoices(pkg.ref);
131
130
 
132
131
  // Every outbound string on this page was handed to the index by a registry
133
132
  // it does not control, so each one goes through the scheme allowlist before
@@ -162,167 +161,246 @@ const supportChannels = [
162
161
  // taking a row of its own and putting 40 hex characters in the rail.
163
162
  const updated = lastUpdated(pkg);
164
163
  const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : updated;
164
+
165
+ // The card's vote badge, on the page the card links to. Same two halves and
166
+ // the same rule: the count opens the forge thread, because no forge offers a
167
+ // URL that casts a vote, and the extension's `/vote?repo=` route is the only
168
+ // one that does — behind its own disclosure modal, never on this link alone.
169
+ const votes = pkg.rating ? `${pkg.rating.up} upvote${pkg.rating.up === 1 ? "" : "s"}` : "";
170
+ const thread = externalUrl(pkg.rating?.url);
171
+ const voteUrl = vscodeVoteUrl(config.vscodeExtension, pkg.ref);
172
+
173
+ // The kind as texture, the way a card wears it. A retired package spends it
174
+ // on the warning instead — the kind is written in the badge either way.
175
+ const headMark = pkg.deprecated ? DEPRECATED_MARK : KIND_MARKS[pkg.kind];
176
+ const headMarkColor = pkg.deprecated
177
+ ? "var(--grim-color-deprecated)"
178
+ : `var(--grim-color-kind-${pkg.kind}, var(--grim-color-muted))`;
165
179
  ---
166
180
 
167
181
  <Base title={`${title} — ${config.brand}`} description={description} image={pkg.logo}>
168
182
  <div class="detail">
169
- <header class="detail-head" data-slot="detail-header">
170
- {
171
- pkg.logo ? (
172
- // The placeholder ships in the markup and the image fades in over
173
- // it, so a slow or failing load never flashes a broken glyph. The
174
- // slot starts `loading`; the handler in `Base.astro` moves it to
175
- // `ready` or `broken`. Icon mirrors `LogoMark` in `Catalog.tsx`.
176
- <span class="tile logo-slot" data-static data-state="loading">
177
- {/* Both marks ship; CSS shows one per state. The card grid picks
178
- between them in Preact instead, but this page has to settle
179
- its placeholder before any script runs. */}
180
- <Image class="logo-mark mark-loading" aria-hidden="true" />
181
- <ImageOff class="logo-mark mark-broken" aria-hidden="true" />
182
- <img src={withBase(pkg.logo)} alt="" />
183
- </span>
184
- ) : (
185
- <div class="tile letter" style={`background: var(--grim-color-kind-${pkg.kind}, var(--grim-color-muted))`}>
186
- {pkg.name[0]?.toUpperCase()}
187
- </div>
188
- )
189
- }
190
- <div class="detail-head-text">
191
- <h1>
192
- {pkg.name} <span class={`badge kind-${pkg.kind}`}>{pkg.kind}</span>
193
- {/* Skills only — every other kind reports null, and grim's own
194
- surfaces show it beside the kind for the same reason: it
195
- qualifies what the thing is, not what it says about itself. */}
196
- {pkg.compatibility && <span class="badge compat">{pkg.compatibility}</span>}
197
- </h1>
198
- {description && <p class="summary">{description}</p>}
199
- {byline && <p class="byline">by {byline}</p>}
200
- </div>
201
- </header>
202
-
203
- {
204
- pkg.deprecated && (
205
- <p class="deprecated-banner" data-slot="deprecated-banner">
206
- ⚠️ deprecated{pkg.replacedBy ? ` — replaced by ${pkg.replacedBy}` : ""}
207
- </p>
208
- )
209
- }
210
-
211
- <div class="cmd-bar" data-os-switch data-os-noun={`add command for ${pkg.name}`}>
212
- <PickerMenu choices={scopeChoices} title="Scope" label="Choose the scope" />
213
- <CommandField
214
- value={scopeChoices[0].command}
215
- name={`${scopeChoices[0].name} add command for ${pkg.name}`}
216
- label="Copy the add command"
217
- />
218
- {
219
- vscode && (
220
- <a
221
- class="seg brand-link"
222
- href={vscode}
223
- title="Open in VS Code"
224
- aria-label={`Open ${pkg.name} in VS Code`}
225
- >
226
- <BrandMark path={mdiMicrosoftVisualStudioCode} />
227
- </a>
228
- )
229
- }
230
- </div>
231
-
232
183
  <div class="detail-body" data-slot="detail-body">
233
- <main class="detail-main">
234
- {/* Radio tabs, not a scripted tablist: the ribbon works on a page
235
- whose JS never ran, and a radio group already has the keyboard
236
- behaviour tabs want (arrows move, only one selected). The inputs
237
- are visually hidden rather than `hidden`, which would take them
238
- out of the focus order along with the pixels. */}
239
- {/* All three tabs are always offered a ribbon that grows a tab per
240
- package makes the panel a different shape on every page. An empty
241
- one is disabled instead: greyed, not clickable through to a
242
- sentence apologising for itself. */}
243
- <input
244
- type="radio"
245
- name="panel"
246
- id="panel-readme"
247
- class="sr-only"
248
- checked={openTab === "readme"}
249
- disabled={!filled.readme}
250
- />
251
- <input
252
- type="radio"
253
- name="panel"
254
- id="panel-contents"
255
- class="sr-only"
256
- checked={openTab === "contents"}
257
- disabled={!filled.contents}
258
- />
259
- <input
260
- type="radio"
261
- name="panel"
262
- id="panel-changelog"
263
- class="sr-only"
264
- checked={openTab === "changelog"}
265
- disabled={!filled.changelog}
266
- />
267
- {/* The greyed tab says it is not for clicking; the tooltip says why,
268
- so the answer is one hover away instead of a panel of its own. */}
269
- <div class="ribbon">
270
- <label for="panel-readme" title={filled.readme ? undefined : "no readme available"}>
271
- Readme
272
- </label>
273
- <label for="panel-contents" title={filled.contents ? undefined : "no contents available"}>
274
- Contents
275
- </label>
276
- <label
277
- for="panel-changelog"
278
- title={filled.changelog ? undefined : "no changelog available"}
279
- >
280
- Changelog
281
- </label>
282
- </div>
283
- {filled.readme && (
284
- <div class="prose tab-panel" id="tab-readme">
285
- <Readme />
184
+ <div class="detail-col">
185
+ <header class="detail-head" data-slot="detail-header">
186
+ {
187
+ pkg.logo ? (
188
+ // The placeholder ships in the markup and the image fades in over
189
+ // it, so a slow or failing load never flashes a broken glyph. The
190
+ // slot starts `loading`; the handler in `Base.astro` moves it to
191
+ // `ready` or `broken`. Icon mirrors `LogoMark` in `Catalog.tsx`.
192
+ <span class="tile logo-slot" data-static data-state="loading">
193
+ {/* Both marks ship; CSS shows one per state. The card grid picks
194
+ between them in Preact instead, but this page has to settle
195
+ its placeholder before any script runs. */}
196
+ <Image class="logo-mark mark-loading" aria-hidden="true" />
197
+ <ImageOff class="logo-mark mark-broken" aria-hidden="true" />
198
+ <img src={withBase(pkg.logo)} alt="" />
199
+ </span>
200
+ ) : (
201
+ <div class="tile letter" style={`background: var(--grim-color-kind-${pkg.kind}, var(--grim-color-muted))`}>
202
+ {pkg.name[0]?.toUpperCase()}
203
+ </div>
204
+ )
205
+ }
206
+ <div class="detail-head-text">
207
+ <h1>
208
+ {pkg.name} <span class={`badge kind-${pkg.kind}`}>{pkg.kind}</span>
209
+ {/* Skills only — every other kind reports null, and grim's own
210
+ surfaces show it beside the kind for the same reason: it
211
+ qualifies what the thing is, not what it says about itself. */}
212
+ {pkg.compatibility && (
213
+ <span class="badge compat" title={`Built for ${pkg.compatibility}`}>
214
+ {pkg.compatibility}
215
+ </span>
216
+ )}
217
+ </h1>
218
+ {description && <p class="summary">{description}</p>}
219
+ {byline && <p class="byline">by {byline}</p>}
286
220
  </div>
287
- )}
221
+ </header>
222
+
288
223
  {
289
- filled.contents && (
290
- <div class="prose tab-panel" id="tab-contents">
291
- {Contents ? (
292
- <Contents />
293
- ) : members.length > 0 ? (
294
- <ul class="members">
295
- {members.map((member) => (
296
- <li>
297
- {member.href ? (
298
- <a href={member.href}>{member.name}</a>
299
- ) : (
300
- <span>{member.name}</span>
301
- )}
302
- <span class={`badge kind-${member.kind}`}>{member.kind}</span>
303
- <code>{member.ref}</code>
304
- </li>
305
- ))}
306
- </ul>
307
- ) : (
308
- <Code
309
- code={JSON.stringify(contentsData, null, 2)}
310
- lang="json"
311
- themes={SHIKI_THEMES}
312
- />
313
- )}
314
- </div>
224
+ pkg.deprecated && (
225
+ <p class="deprecated-banner" data-slot="deprecated-banner">
226
+ {/* Drawn, not an emoji: ⚠️ renders as a different glyph on every
227
+ platform, takes the system's colour rather than the token's,
228
+ and sits off the text baseline. Every other icon on this site
229
+ comes from one stroked set; this one had no reason not to. */}
230
+ <TriangleAlert size={16} aria-hidden="true" />
231
+ {deprecationNote(pkg)}
232
+ </p>
315
233
  )
316
234
  }
317
- {filled.changelog && (
318
- <div class="prose tab-panel" id="tab-changelog">
319
- <Changelog />
235
+
236
+ <CommandBar
237
+ choices={scopeChoices}
238
+ noun={`add command for ${pkg.name}`}
239
+ copyLabel="Copy the add command"
240
+ pickerTitle="Scope"
241
+ pickerLabel="Choose the scope"
242
+ >
243
+ {
244
+ vscode && (
245
+ <a
246
+ slot="action"
247
+ class="seg brand-link"
248
+ href={vscode}
249
+ title="Open in VS Code"
250
+ aria-label={`Open ${pkg.name} in VS Code`}
251
+ >
252
+ <BrandMark path={mdiMicrosoftVisualStudioCode} />
253
+ </a>
254
+ )
255
+ }
256
+ </CommandBar>
257
+
258
+ <main class="detail-main">
259
+ {/* The kind as texture behind the panel, the way a card wears it:
260
+ oversized and hung off the panel's bottom-right corner, so
261
+ `overflow: hidden` crops it to the rounded edge. Decorative — the
262
+ kind is a badge beside the name — so it is aria-hidden unless it
263
+ is the warning a retired package wears instead. */}
264
+ {
265
+ headMark && (
266
+ <KindMark
267
+ glyph={headMark}
268
+ class="panel-watermark"
269
+ size={220}
270
+ role={pkg.deprecated ? "img" : undefined}
271
+ aria-label={pkg.deprecated ? "deprecated" : undefined}
272
+ aria-hidden={pkg.deprecated ? undefined : "true"}
273
+ style={`color: ${headMarkColor}`}
274
+ />
275
+ )
276
+ }
277
+ {/* Radio tabs, not a scripted tablist: the ribbon works on a page
278
+ whose JS never ran, and a radio group already has the keyboard
279
+ behaviour tabs want (arrows move, only one selected). The inputs
280
+ are visually hidden rather than `hidden`, which would take them
281
+ out of the focus order along with the pixels. */}
282
+ {/* All three tabs are always offered — a ribbon that grows a tab per
283
+ package makes the panel a different shape on every page. An empty
284
+ one is disabled instead: greyed, not clickable through to a
285
+ sentence apologising for itself. */}
286
+ <input
287
+ type="radio"
288
+ name="panel"
289
+ id="panel-readme"
290
+ class="sr-only"
291
+ checked={openTab === "readme"}
292
+ disabled={!filled.readme}
293
+ />
294
+ <input
295
+ type="radio"
296
+ name="panel"
297
+ id="panel-contents"
298
+ class="sr-only"
299
+ checked={openTab === "contents"}
300
+ disabled={!filled.contents}
301
+ />
302
+ <input
303
+ type="radio"
304
+ name="panel"
305
+ id="panel-changelog"
306
+ class="sr-only"
307
+ checked={openTab === "changelog"}
308
+ disabled={!filled.changelog}
309
+ />
310
+ {/* The greyed tab says it is not for clicking; the tooltip says why,
311
+ so the answer is one hover away instead of a panel of its own. */}
312
+ <div class="ribbon">
313
+ <label for="panel-readme" title={filled.readme ? undefined : "no readme available"}>
314
+ Readme
315
+ </label>
316
+ <label for="panel-contents" title={filled.contents ? undefined : "no contents available"}>
317
+ Contents
318
+ </label>
319
+ <label
320
+ for="panel-changelog"
321
+ title={filled.changelog ? undefined : "no changelog available"}
322
+ >
323
+ Changelog
324
+ </label>
325
+ {
326
+ pkg.rating && (
327
+ <span class="rating-group">
328
+ {thread ? (
329
+ <a
330
+ class="rating-count"
331
+ href={thread}
332
+ target="_blank"
333
+ rel="noopener noreferrer"
334
+ title={`${votes} — open the thread to vote`}
335
+ aria-label={`${votes}. Open the voting thread`}
336
+ >
337
+ <ArrowBigUp size={13} aria-hidden="true" />
338
+ {pkg.rating.up}
339
+ </a>
340
+ ) : (
341
+ <span class="rating-count" title={votes}>
342
+ <ArrowBigUp size={13} aria-hidden="true" />
343
+ {pkg.rating.up}
344
+ </span>
345
+ )}
346
+ {voteUrl && (
347
+ <a
348
+ class="rating-vote"
349
+ href={voteUrl}
350
+ title="Upvote in VS Code"
351
+ aria-label={`Upvote ${pkg.name} in VS Code`}
352
+ >
353
+ <BrandMark path={mdiMicrosoftVisualStudioCode} size={12} />
354
+ </a>
355
+ )}
356
+ </span>
357
+ )
358
+ }
320
359
  </div>
321
- )}
322
- {/* Nothing anywhere: one statement in the middle of the panel, rather
323
- than three tabs each denying it separately. */}
324
- {openTab === null && <p class="panel-empty">No content available.</p>}
325
- </main>
360
+ {filled.readme && (
361
+ <div class="prose tab-panel" id="tab-readme">
362
+ <Readme />
363
+ </div>
364
+ )}
365
+ {
366
+ filled.contents && (
367
+ <div class="prose tab-panel" id="tab-contents">
368
+ {Contents ? (
369
+ <Contents />
370
+ ) : members.length > 0 ? (
371
+ <ul class="members">
372
+ {members.map((member) => (
373
+ <li>
374
+ {member.href ? (
375
+ <a href={member.href}>{member.name}</a>
376
+ ) : (
377
+ <span>{member.name}</span>
378
+ )}
379
+ <span class={`badge kind-${member.kind}`}>{member.kind}</span>
380
+ <code>{member.ref}</code>
381
+ </li>
382
+ ))}
383
+ </ul>
384
+ ) : (
385
+ <Code
386
+ code={JSON.stringify(contentsData, null, 2)}
387
+ lang="json"
388
+ themes={SHIKI_THEMES}
389
+ />
390
+ )}
391
+ </div>
392
+ )
393
+ }
394
+ {filled.changelog && (
395
+ <div class="prose tab-panel" id="tab-changelog">
396
+ <Changelog />
397
+ </div>
398
+ )}
399
+ {/* Nothing anywhere: one statement in the middle of the panel, rather
400
+ than three tabs each denying it separately. */}
401
+ {openTab === null && <p class="panel-empty">No content available.</p>}
402
+ </main>
403
+ </div>
326
404
 
327
405
  <aside class="detail-rail" data-slot="detail-rail">
328
406
  {tags.length > 0 && (
@@ -349,6 +427,22 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
349
427
  </section>
350
428
  )}
351
429
 
430
+ {supportChannels.length > 0 && (
431
+ <section class="support">
432
+ <h2>Get help</h2>
433
+ <ul>
434
+ {supportChannels.map(({ label, href, Icon }) => (
435
+ <li>
436
+ <Icon size={15} aria-hidden="true" />
437
+ <a href={href} target="_blank" rel="noopener noreferrer">
438
+ {label}
439
+ </a>
440
+ </li>
441
+ ))}
442
+ </ul>
443
+ </section>
444
+ )}
445
+
352
446
  <section>
353
447
  <h2>Details</h2>
354
448
  <dl>
@@ -435,10 +529,13 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
435
529
  <ul class="keywords">
436
530
  {pkg.keywords.map((k) => (
437
531
  <li>
438
- {/* Back to the catalog with the term already in the search
439
- box — a keyword is only useful if it leads somewhere. */}
532
+ {/* Back to the catalog with the keyword FACET applied, not
533
+ a text search. `?q=` also matched every description with
534
+ the word in it, so a link labelled `cli` returned
535
+ packages nobody tagged `cli`. The catalog still reads
536
+ `?q=`, so links shared before this change still work. */}
440
537
  <a
441
- href={withBase(`/?q=${encodeURIComponent(k)}`)}
538
+ href={withBase(`/?kw=${encodeURIComponent(k)}`)}
442
539
  title={`Find packages tagged ${k}`}
443
540
  >
444
541
  {k}
@@ -449,21 +546,6 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
449
546
  )}
450
547
  </section>
451
548
 
452
- {supportChannels.length > 0 && (
453
- <section class="support">
454
- <h2>Get help</h2>
455
- <ul>
456
- {supportChannels.map(({ label, href, Icon }) => (
457
- <li>
458
- <Icon size={15} aria-hidden="true" />
459
- <a href={href} target="_blank" rel="noopener noreferrer">
460
- {label}
461
- </a>
462
- </li>
463
- ))}
464
- </ul>
465
- </section>
466
- )}
467
549
  </aside>
468
550
  </div>
469
551
  </div>
@@ -520,20 +602,42 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
520
602
  flex-direction: column;
521
603
  gap: var(--grim-space-7);
522
604
  }
605
+ /* The left column: header, install bar and panel, all to the width of the
606
+ README rather than to the width of the page. The rail then runs the
607
+ full height beside them instead of starting a screenful down, which is
608
+ what left the top-right corner of the page empty. */
609
+ .detail-col {
610
+ display: flex;
611
+ flex-direction: column;
612
+ gap: var(--grim-space-7);
613
+ min-width: 0;
614
+ }
523
615
  .detail-head {
524
616
  display: flex;
525
- align-items: center;
617
+ /* Stretch, so the logo can take the height of the whole text block
618
+ beside it rather than sitting centred against a third of it. */
619
+ align-items: stretch;
526
620
  gap: var(--grim-space-6);
527
621
  background: var(--grim-color-card);
528
622
  border: var(--grim-border-width) solid var(--grim-color-border);
529
- border-radius: var(--grim-radius-xl);
623
+ border-radius: var(--grim-radius-surface);
530
624
  padding: var(--grim-space-6) var(--grim-space-7);
531
625
  }
626
+ .detail-head-text {
627
+ min-width: 0;
628
+ }
532
629
  .tile {
533
- flex-shrink: 0;
534
- width: 3.5rem;
535
- height: 3.5rem;
536
- border-radius: var(--grim-radius-xl);
630
+ flex: 0 0 auto;
631
+ align-self: stretch;
632
+ width: auto;
633
+ height: auto;
634
+ /* Square, sized by the text block it stands beside. Bounded at both
635
+ ends: a one-line header must not shrink it to a favicon, and a long
636
+ summary must not grow it into the page. */
637
+ aspect-ratio: 1;
638
+ min-height: 3.5rem;
639
+ max-height: 7rem;
640
+ border-radius: var(--grim-radius-surface);
537
641
  object-fit: cover;
538
642
  }
539
643
  .tile.letter {
@@ -554,11 +658,14 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
554
658
  font-family: ui-monospace, "SFMono-Regular", monospace;
555
659
  }
556
660
  .summary {
557
- margin: var(--grim-space-2) 0 0;
661
+ margin: var(--grim-space-4) 0 0;
558
662
  color: var(--grim-color-muted);
559
663
  }
664
+ /* Tight to the summary above it. Evenly spaced between the name and the
665
+ summary it read as a third heading; who publishes this is a footnote on
666
+ the sentence, so it sits with the sentence. */
560
667
  .byline {
561
- margin: var(--grim-space-3) 0 0;
668
+ margin: var(--grim-space-1) 0 0;
562
669
  color: var(--grim-color-muted);
563
670
  font-size: var(--grim-text-sm);
564
671
  }
@@ -590,16 +697,20 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
590
697
  }
591
698
  .deprecated-banner {
592
699
  margin: 0;
700
+ /* The glyph sits on the sentence's line rather than above it. */
701
+ display: flex;
702
+ align-items: center;
703
+ gap: var(--grim-space-3);
593
704
  background: var(--grim-color-banner-bg);
594
705
  color: var(--grim-color-banner-fg);
595
706
  border: var(--grim-border-width) solid var(--grim-color-banner-border);
596
- border-radius: var(--grim-radius-lg);
707
+ border-radius: var(--grim-radius-control);
597
708
  padding: var(--grim-space-4) var(--grim-space-6);
598
709
  font-weight: 600;
599
710
  }
600
711
  .detail-body {
601
712
  display: grid;
602
- grid-template-columns: 1fr 18rem;
713
+ grid-template-columns: minmax(0, 1fr) 18rem;
603
714
  gap: var(--grim-space-8);
604
715
  align-items: start;
605
716
  }
@@ -612,8 +723,36 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
612
723
  min-width: 0;
613
724
  background: var(--grim-color-card);
614
725
  border: var(--grim-border-width) solid var(--grim-color-border);
615
- border-radius: var(--grim-radius-xl);
726
+ border-radius: var(--grim-radius-surface);
616
727
  padding: var(--grim-space-7) var(--grim-space-7);
728
+ /* Crops the oversized kind mark to the rounded edge and gives it a
729
+ stacking context to sit behind in — without `isolation` a
730
+ `z-index: -1` child escapes to the root and vanishes under the
731
+ panel's own background. */
732
+ position: relative;
733
+ overflow: hidden;
734
+ isolation: isolate;
735
+ }
736
+ /* Hung off the panel's bottom-right corner, cropped by the panel's own
737
+ rounded edge. Opacity on the element rather than in the colour: the
738
+ glyph's paths overlap, and a translucent fill composites every crossing
739
+ twice and comes out blotchy along its own joins. */
740
+ .detail-main :global(.panel-watermark) {
741
+ position: absolute;
742
+ z-index: -1;
743
+ opacity: 0.14;
744
+ bottom: 0;
745
+ right: 0;
746
+ /* Same 26% of its own box as the card's mark — see `.card-watermark`
747
+ in `Base.astro` for why this is a translate and not an inset. */
748
+ transform: translate(26%, 26%);
749
+ pointer-events: none;
750
+ }
751
+ /* The vote rides the ribbon's right end — the panel's own row of
752
+ controls, and the one line on this page that was already a toolbar. */
753
+ .ribbon .rating-group {
754
+ align-self: center;
755
+ margin-left: auto;
617
756
  }
618
757
  /* Markdown opens with its own heading, whose margin lands on top of the
619
758
  panel padding and reads as a hole. Every panel sets its own top edge. */
@@ -692,7 +831,7 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
692
831
  #panel-changelog:focus-visible ~ .ribbon label[for="panel-changelog"] {
693
832
  outline: 2px solid var(--grim-color-accent);
694
833
  outline-offset: 2px;
695
- border-radius: var(--grim-radius-sm);
834
+ border-radius: var(--grim-radius-code);
696
835
  }
697
836
 
698
837
  /* A bundle's member list: what it installs, one row each. A grid, not a
@@ -735,17 +874,23 @@ const updatedTitle = pkg.revision ? `${updated} · revision ${pkg.revision}` : u
735
874
  grid-column: 1 / -1;
736
875
  }
737
876
  }
877
+ /* One panel, not three cards stacked. Beside a single tall README panel a
878
+ column of separately bordered boxes read as debris; the sections are
879
+ still separated, by a rule inside the panel rather than by a gap and a
880
+ second border each. */
738
881
  .detail-rail {
739
882
  display: flex;
740
883
  flex-direction: column;
741
- gap: var(--grim-space-7);
742
- }
743
- .detail-rail section {
744
884
  background: var(--grim-color-card);
745
885
  border: var(--grim-border-width) solid var(--grim-color-border);
746
- border-radius: var(--grim-radius-xl);
886
+ border-radius: var(--grim-radius-surface);
887
+ }
888
+ .detail-rail section {
747
889
  padding: var(--grim-space-6);
748
890
  }
891
+ .detail-rail section + section {
892
+ border-top: var(--grim-border-width) solid var(--grim-color-border);
893
+ }
749
894
  /* Panel label. The main panel's ribbon labels match this by hand — they
750
895
  are <label>s, not headings, so they cannot join the selector. */
751
896
  .detail-rail h2 {