@grimoire-rs/indexer 0.4.4 → 0.5.1

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