@grimoire-rs/indexer 0.4.4 → 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 (60) 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/gitignore +4 -1
  59. package/templates/theme/README.md +38 -0
  60. package/templates/tsconfig.json +47 -0
@@ -0,0 +1,273 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+
4
+ // One card of the grid view.
5
+ //
6
+ // Its own file so an index can replace the card without owning `Catalog.tsx`
7
+ // and its sorting, filtering, keyboard and URL-state machinery. Unlike the
8
+ // `.astro` components, this one takes props — a Preact component in a
9
+ // hydrated island cannot read the build-time payload — so its props ARE a
10
+ // contract, and they are the reason component overrides are documented as
11
+ // unstable for now.
12
+ import { ArrowBigUp } from "lucide-preact";
13
+ import { mdiMicrosoftVisualStudioCode } from "@mdi/js";
14
+ import { BrandMark } from "./BrandMark.js";
15
+ import { CardLogo } from "./CardLogo.js";
16
+ import { CopyButton } from "./CopyButton.js";
17
+ import { DEPRECATED_MARK, KIND_MARKS, KindMark } from "./KindMark.js";
18
+ import { withBase } from "../lib/base.js";
19
+ import {
20
+ externalUrl,
21
+ lastUpdated,
22
+ timeAgo,
23
+ vscodeUrl,
24
+ vscodeVoteUrl,
25
+ type CatalogPackage,
26
+ } from "../lib/catalog.js";
27
+
28
+ export interface PackageCardProps {
29
+ pkg: CatalogPackage;
30
+ /**
31
+ * `publisher.extension` id behind the deep links, or `null`. A prop rather
32
+ * than a `lib/data` read: this island hydrates in the browser, and importing
33
+ * the build-time payload here would ship the whole catalog twice.
34
+ */
35
+ vscodeExtension: string | null;
36
+ /** Keyword facets currently applied, so a chip can render itself pressed. */
37
+ activeKeywords: string[];
38
+ /** Apply or clear one keyword facet. */
39
+ onToggleKeyword: (keyword: string) => void;
40
+ /** The catalog's own arrow-key navigation. */
41
+ onKeyDown: (event: KeyboardEvent) => void;
42
+ }
43
+
44
+ export function PackageCard({
45
+ pkg: p,
46
+ vscodeExtension,
47
+ activeKeywords,
48
+ onToggleKeyword,
49
+ onKeyDown,
50
+ }: PackageCardProps) {
51
+ return (
52
+ <li
53
+ class="card"
54
+ data-slot="package-card"
55
+ tabIndex={0}
56
+ onKeyDown={onKeyDown}
57
+ >
58
+ {/* The kind again, as texture: the same mark the card's kind
59
+ wears in the VS Code extension, enlarged into the card's
60
+ top-right corner. Decorative — the kind is written on the
61
+ address line — so it is aria-hidden.
62
+
63
+ Every mark is framed on its own bounding box (see
64
+ `KindMark`), so they render at one apparent size rather
65
+ than at whatever fraction of the 16-unit grid each codicon
66
+ happens to use.
67
+
68
+ The colour is the token at FULL strength; the fading is
69
+ `opacity` on the element, in CSS. A translucent colour is
70
+ not equivalent: any glyph whose paths overlap composites
71
+ each crossing twice and comes out blotchy along its own
72
+ joins. Element opacity flattens the mark first and blends
73
+ the result once, which is the only way the tint is even.
74
+
75
+ A deprecated package spends it on the warning instead. The
76
+ kind is written either way, so the mark is free to carry the
77
+ one thing that has no words — and being the only deprecation
78
+ signal on the card, it is labelled rather than hidden. */}
79
+ {(() => {
80
+ const mark = p.deprecated
81
+ ? DEPRECATED_MARK
82
+ : KIND_MARKS[p.kind];
83
+ if (!mark) return null;
84
+ return (
85
+ <KindMark
86
+ glyph={mark}
87
+ class="card-watermark"
88
+ size={112}
89
+ role={p.deprecated ? "img" : undefined}
90
+ aria-label={p.deprecated ? "deprecated" : undefined}
91
+ aria-hidden={p.deprecated ? undefined : "true"}
92
+ style={{
93
+ color: p.deprecated
94
+ ? "var(--grim-color-deprecated)"
95
+ : `var(--grim-color-kind-${p.kind}, var(--grim-color-muted))`,
96
+ }}
97
+ />
98
+ );
99
+ })()}
100
+ <div class="card-head">
101
+ <CardLogo pkg={p} />
102
+ <h2 data-slot="package-name">
103
+ <a
104
+ href={withBase(`/p/${p.namespace}/${p.name}/`)}
105
+ tabIndex={-1}
106
+ >
107
+ {p.name}
108
+ </a>
109
+ </h2>
110
+ {/* A count and two ways to add to it — never a "you voted"
111
+ state: the page is prerendered once for everyone, so it
112
+ cannot know whether *you* did, and showing "not voted"
113
+ to someone who has would be worse than showing nothing.
114
+
115
+ Left, the count itself, linking to the forge thread the
116
+ sidecar names. No forge offers a URL that casts a vote,
117
+ so this opens the thread and the reader clicks the
118
+ reaction there. Right, the extension's `/vote?repo=`
119
+ route, which does cast one — behind its own disclosure
120
+ modal, never on the strength of this link. */}
121
+ {p.rating &&
122
+ (() => {
123
+ const votes = `${p.rating.up} upvote${p.rating.up === 1 ? "" : "s"}`;
124
+ // Registry-supplied, like every other outbound string
125
+ // here: through the scheme allowlist before it is an
126
+ // href, whatever the sidecar spelled.
127
+ const thread = externalUrl(p.rating.url);
128
+ const vote = vscodeVoteUrl(vscodeExtension, p.ref);
129
+ return (
130
+ <span class="rating-group">
131
+ {thread ? (
132
+ <a
133
+ class="rating-count"
134
+ href={thread}
135
+ target="_blank"
136
+ rel="noopener noreferrer"
137
+ tabIndex={-1}
138
+ title={`${votes} — open the thread to vote`}
139
+ aria-label={`${p.name}: ${votes}. Open the voting thread`}
140
+ >
141
+ <ArrowBigUp size={13} aria-hidden="true" />
142
+ {p.rating.up}
143
+ </a>
144
+ ) : (
145
+ <span class="rating-count" title={votes}>
146
+ <ArrowBigUp size={13} aria-hidden="true" />
147
+ {p.rating.up}
148
+ </span>
149
+ )}
150
+ {vote && (
151
+ <a
152
+ class="rating-vote"
153
+ href={vote}
154
+ tabIndex={-1}
155
+ title="Upvote in VS Code"
156
+ aria-label={`Upvote ${p.name} in VS Code`}
157
+ >
158
+ <BrandMark
159
+ path={mdiMicrosoftVisualStudioCode}
160
+ size={12}
161
+ />
162
+ </a>
163
+ )}
164
+ </span>
165
+ );
166
+ })()}
167
+ {/* Kind, then where it lives — two things, dot-separated, in
168
+ the same shape the foot's `version · updated` uses. The
169
+ kind leads because it is what a reader filters on; the
170
+ watermark in the corner says the same thing without
171
+ words. Deprecation is deliberately NOT here: a third item
172
+ filled the line, and the watermark carries it. */}
173
+ <p class="namespace">
174
+ <span class="kind" data-slot="package-kind">
175
+ {p.kind}
176
+ </span>
177
+ <span aria-hidden="true"> · </span>
178
+ {p.namespace}
179
+ </p>
180
+ </div>
181
+ {/* Under the head, above the description: what the package
182
+ is tagged with. One row, never two — the row gives up its
183
+ overflow rather than wrapping, and fades at its right end
184
+ to say there is more. */}
185
+ {p.keywords && p.keywords.length > 0 && (
186
+ <div class="keywords" data-slot="package-keywords">
187
+ {/* Capped at five so a package with thirty keywords does
188
+ not render thirty chips off the side of a clipped row.
189
+ Which of the five actually fit is the row's business. */}
190
+ {/* Applies the keyword facet, not a text search. The two
191
+ are not the same answer: `setQuery("cli")` also matched
192
+ every description with the word in it, so the chip
193
+ returned packages that are not tagged `cli` at all. */}
194
+ {p.keywords.slice(0, 5).map((kw) => (
195
+ <button
196
+ key={kw}
197
+ type="button"
198
+ class={
199
+ activeKeywords.includes(kw)
200
+ ? "chip keyword active"
201
+ : "chip keyword"
202
+ }
203
+ aria-pressed={activeKeywords.includes(kw)}
204
+ tabIndex={-1}
205
+ onClick={() => onToggleKeyword(kw)}
206
+ >
207
+ {kw}
208
+ </button>
209
+ ))}
210
+ </div>
211
+ )}
212
+ {p.description && <p class="description">{p.description}</p>}
213
+ <div class="card-foot">
214
+ <div class="copy-group">
215
+ {/* Global first, matching the hero's scope picker — the two
216
+ are the same choice in two places, so they lead with the
217
+ same one. */}
218
+ <CopyButton
219
+ command={`grim add --global ${p.ref}`}
220
+ variant="global"
221
+ name={`global add for ${p.name}`}
222
+ />
223
+ <CopyButton
224
+ command={`grim add ${p.ref}`}
225
+ name={`project add for ${p.name}`}
226
+ />
227
+ {vscodeUrl(vscodeExtension, p.ref) && (
228
+ <a
229
+ class="copy vscode"
230
+ href={vscodeUrl(vscodeExtension, p.ref)!}
231
+ title="Open in VS Code"
232
+ aria-label={`Open ${p.name} in VS Code`}
233
+ tabIndex={-1}
234
+ >
235
+ <BrandMark path={mdiMicrosoftVisualStudioCode} />
236
+ </a>
237
+ )}
238
+ </div>
239
+ {/* Version and recency, on their own line under the buttons.
240
+ Both answer the same question — how current is this — so
241
+ they read as one stamp; alongside the buttons they were a
242
+ third thing competing for the card's last row, which is
243
+ where the vote badge now sits.
244
+
245
+ The date is the one the sidecar derived, not the
246
+ artifact's own `created`: a package republished from the
247
+ same commit keeps its date, and one with no commit date
248
+ at all gets the day this index first saw its current
249
+ digest. Licence is gone from the card entirely; it
250
+ matters when adopting a package, not when scanning for
251
+ one, and the detail page states it. */}
252
+ {(() => {
253
+ const at = lastUpdated(p);
254
+ const ago = at && timeAgo(at) ? timeAgo(at) : null;
255
+ if (!p.version && !ago) return null;
256
+ return (
257
+ <p class="card-meta" data-slot="package-meta">
258
+ {p.version && (
259
+ <span class="card-version">v{p.version}</span>
260
+ )}
261
+ {p.version && ago && <span aria-hidden="true"> · </span>}
262
+ {ago && at && (
263
+ <time datetime={at} title={at}>
264
+ updated {ago}
265
+ </time>
266
+ )}
267
+ </p>
268
+ );
269
+ })()}
270
+ </div>
271
+ </li>
272
+ );
273
+ }
@@ -0,0 +1,10 @@
1
+ import { type CatalogPackage } from "../lib/catalog.js";
2
+ export interface PackageRowProps {
3
+ pkg: CatalogPackage;
4
+ /** Whether this index publishes ratings at all — decided once, not per row. */
5
+ hasRatings: boolean;
6
+ /** The catalog's own arrow-key navigation. */
7
+ onKeyDown: (event: KeyboardEvent) => void;
8
+ }
9
+ export declare function PackageRow({ pkg: p, hasRatings, onKeyDown }: PackageRowProps): import("preact").JSX.Element;
10
+ //# sourceMappingURL=PackageRow.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright 2026 The Grimoire Authors
4
+ // One row of the table view.
5
+ //
6
+ // Its own file so an index can replace the row without owning `Catalog.tsx`
7
+ // and its sorting, filtering, keyboard and URL-state machinery. Unlike the
8
+ // `.astro` components, this one takes props — a Preact component in a
9
+ // hydrated island cannot read the build-time payload — so its props ARE a
10
+ // contract, and they are the reason component overrides are documented as
11
+ // unstable for now.
12
+ import { ArrowBigUp } from "lucide-preact";
13
+ import { CardLogo } from "./CardLogo.js";
14
+ import { DEPRECATED_MARK, KIND_MARKS, KindMark } from "./KindMark.js";
15
+ import { withBase } from "../lib/base.js";
16
+ import { lastUpdated, timeAgo } from "../lib/catalog.js";
17
+ export function PackageRow({ pkg: p, hasRatings, onKeyDown }) {
18
+ const at = lastUpdated(p);
19
+ const ago = at && timeAgo(at) ? timeAgo(at) : null;
20
+ return (_jsxs("a", { class: "row", "data-slot": "package-row", href: withBase(`/p/${p.namespace}/${p.name}/`),
21
+ // The full identity, since the visible name is clipped and the
22
+ // namespace is not shown at all.
23
+ title: `${p.namespace}/${p.name}`, onKeyDown: onKeyDown, children: [_jsx("span", { class: "t-logo", children: _jsx(CardLogo, { pkg: p }) }), (() => {
24
+ const mark = p.deprecated ? DEPRECATED_MARK : KIND_MARKS[p.kind];
25
+ const label = p.deprecated ? `${p.kind}, deprecated` : p.kind;
26
+ return (_jsx("span", { class: "t-kind", "data-slot": "package-kind", title: label, style: {
27
+ color: p.deprecated
28
+ ? "var(--grim-color-deprecated)"
29
+ : `var(--grim-color-kind-${p.kind}, var(--grim-color-muted))`,
30
+ }, children: mark ? (_jsx(KindMark, { glyph: mark, size: 16, role: "img", "aria-label": label })) : (_jsx("span", { class: "t-kind-word", children: label })) }));
31
+ })(), _jsx("span", { class: "t-name", "data-slot": "package-name", children: p.name }), _jsx("span", { class: "t-desc", children: p.description }), _jsx("span", { class: "t-updated", "data-slot": "package-meta", children: ago && at && (_jsx("time", { datetime: at, title: at, children: ago })) }), hasRatings && (_jsx("span", { class: "t-rating", children: p.rating && (_jsxs(_Fragment, { children: [_jsx("span", { class: "t-votes", children: p.rating.up }), _jsx(ArrowBigUp, { size: 13, "aria-hidden": "true" })] })) }))] }));
32
+ }
@@ -0,0 +1,126 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+
4
+ // One row of the table view.
5
+ //
6
+ // Its own file so an index can replace the row without owning `Catalog.tsx`
7
+ // and its sorting, filtering, keyboard and URL-state machinery. Unlike the
8
+ // `.astro` components, this one takes props — a Preact component in a
9
+ // hydrated island cannot read the build-time payload — so its props ARE a
10
+ // contract, and they are the reason component overrides are documented as
11
+ // unstable for now.
12
+ import { ArrowBigUp } from "lucide-preact";
13
+ import { CardLogo } from "./CardLogo.js";
14
+ import { DEPRECATED_MARK, KIND_MARKS, KindMark } from "./KindMark.js";
15
+ import { withBase } from "../lib/base.js";
16
+ import { lastUpdated, timeAgo, type CatalogPackage } from "../lib/catalog.js";
17
+
18
+ export interface PackageRowProps {
19
+ pkg: CatalogPackage;
20
+ /** Whether this index publishes ratings at all — decided once, not per row. */
21
+ hasRatings: boolean;
22
+ /** The catalog's own arrow-key navigation. */
23
+ onKeyDown: (event: KeyboardEvent) => void;
24
+ }
25
+
26
+ export function PackageRow({ pkg: p, hasRatings, onKeyDown }: PackageRowProps) {
27
+ const at = lastUpdated(p);
28
+ const ago = at && timeAgo(at) ? timeAgo(at) : null;
29
+ return (
30
+ <a
31
+ class="row"
32
+ data-slot="package-row"
33
+ href={withBase(`/p/${p.namespace}/${p.name}/`)}
34
+ // The full identity, since the visible name is clipped and the
35
+ // namespace is not shown at all.
36
+ title={`${p.namespace}/${p.name}`}
37
+ onKeyDown={onKeyDown}
38
+ >
39
+ {/* The same tile the card shows, at a row's height. It goes in a
40
+ column of its own rather than beside the name so it stays a
41
+ slot the eye tracks down — and `CardLogo` already answers the
42
+ three states a logo has here (missing, loading, declared but
43
+ unreachable), so the column is never ragged. */}
44
+ <span class="t-logo">
45
+ <CardLogo pkg={p} />
46
+ </span>
47
+ {/* The kind as its mark, not as its word — the same glyph the
48
+ card wears in its corner and the same one the VS Code
49
+ extension puts on its cards, in the kind's own colour.
50
+
51
+ Which is also how deprecation is said here: a retired package
52
+ shows the warning mark in the deprecation colour instead of
53
+ its kind, exactly as the card's watermark does. The badge this
54
+ replaces was a second word in a cell with room for none, and
55
+ it widened the column for every row whether or not anything in
56
+ view was retired.
57
+
58
+ The word is not lost — it names the mark, so it is read out,
59
+ and the `title` sits on the cell rather than on the `<svg>`:
60
+ an SVG takes its tooltip from a `<title>` child, and a `title`
61
+ attribute on it does nothing at all. */}
62
+ {(() => {
63
+ const mark = p.deprecated ? DEPRECATED_MARK : KIND_MARKS[p.kind];
64
+ const label = p.deprecated ? `${p.kind}, deprecated` : p.kind;
65
+ return (
66
+ <span
67
+ class="t-kind"
68
+ data-slot="package-kind"
69
+ title={label}
70
+ style={{
71
+ color: p.deprecated
72
+ ? "var(--grim-color-deprecated)"
73
+ : `var(--grim-color-kind-${p.kind}, var(--grim-color-muted))`,
74
+ }}
75
+ >
76
+ {mark ? (
77
+ <KindMark
78
+ glyph={mark}
79
+ size={16}
80
+ role="img"
81
+ aria-label={label}
82
+ />
83
+ ) : (
84
+ // An unknown kind has no mark, and inventing one would be
85
+ // a claim. It keeps the word it always had.
86
+ <span class="t-kind-word">{label}</span>
87
+ )}
88
+ </span>
89
+ );
90
+ })()}
91
+ {/* The name alone. The namespace used to sit beside it and was
92
+ what pushed this cell onto a second line — and it is not worth
93
+ a column of its own here, where the kind already answers "what
94
+ is this" and the description answers "about what". It rides in
95
+ the row's tooltip instead, with the full name, which is also
96
+ what the ellipsis costs the reader. */}
97
+ <span class="t-name" data-slot="package-name">
98
+ {p.name}
99
+ </span>
100
+ <span class="t-desc">{p.description}</span>
101
+ <span class="t-updated" data-slot="package-meta">
102
+ {ago && at && (
103
+ <time datetime={at} title={at}>
104
+ {ago}
105
+ </time>
106
+ )}
107
+ </span>
108
+ {hasRatings && (
109
+ // Count first, arrow after it, both held at the right edge.
110
+ // The count sits in a fixed right-aligned box, so the digits
111
+ // stack into a column and the arrow after them lands in the
112
+ // same place on every row whatever the count is. An unrated
113
+ // package keeps the empty cell: the column is a slot the eye
114
+ // tracks down, and a row that skips it breaks the run.
115
+ <span class="t-rating">
116
+ {p.rating && (
117
+ <>
118
+ <span class="t-votes">{p.rating.up}</span>
119
+ <ArrowBigUp size={13} aria-hidden="true" />
120
+ </>
121
+ )}
122
+ </span>
123
+ )}
124
+ </a>
125
+ );
126
+ }
@@ -8,23 +8,14 @@
8
8
  // `Base.astro`'s `[data-os-switch]` handler moves it. That handler is generic
9
9
  // over bars, so this component ships no script of its own.
10
10
  //
11
- // Icons arrive one of two ways and both render to static SVG at build time:
12
- // `path` for a `@mdi/js` brand mark, `Icon` for a Lucide component. Lucide
13
- // carries no brand marks and MDI is not the set the rest of the site draws
14
- // with, so neither alone covers both bars.
11
+ // Icons arrive one of two ways and both render to static SVG at build time
12
+ // see `Choice` in `../lib/commands`.
15
13
  import { ChevronDown } from "lucide-preact";
16
14
  import { BrandMark } from "./BrandMark.tsx";
15
+ // The shape lives with the functions that build it, so a page importing
16
+ // `installChoices` and a component rendering it cannot disagree about it.
17
+ import type { Choice } from "../lib/commands";
17
18
 
18
- export interface Choice {
19
- /** Shown in the menu, and matched against `data-os-glyph` when picking. */
20
- name: string;
21
- /** What the field shows and copies once this choice is picked. */
22
- command: string;
23
- /** `@mdi/js` path string. */
24
- path?: string;
25
- /** Lucide component, taking a `size` prop. */
26
- Icon?: (props: { size?: number }) => unknown;
27
- }
28
19
 
29
20
  interface Props {
30
21
  choices: Choice[];
@@ -0,0 +1,64 @@
1
+ ---
2
+ // The site footer: the raw-data pointer, whatever links this index adds, and
3
+ // the attribution line.
4
+ //
5
+ // Split out for the same reason as `SiteHeader.astro`, and with the same
6
+ // contract: no props, reads build-time config, styles live in `Base.astro`'s
7
+ // global block. `theme/components/SiteFooter.astro` replaces it.
8
+ import { linkAttrs, withBase } from "../lib/base";
9
+ import { data } from "../lib/data";
10
+
11
+ const { config } = data;
12
+ // Shown as well as linked: on a subpath deployment the bare path is not where
13
+ // the file is, so the label has to carry the prefix too.
14
+ const allJson = withBase("/all.json");
15
+ ---
16
+
17
+ <footer data-slot="site-footer">
18
+ <div class="shell">
19
+ <p>
20
+ Raw data: <a href={allJson}><code>{allJson}</code></a>{
21
+ config.footerNote ? ` · ${config.footerNote}` : ""
22
+ }
23
+ </p>
24
+ {/* Whatever this index has to link — a privacy notice, an imprint, a code
25
+ of conduct, a page it added under `theme/pages/`. Absent by default, so
26
+ a footer without them looks exactly as it did before.
27
+
28
+ `withBase` for the same reason the nav needs it: a site-root href is a
29
+ legal entry here, and on a project Pages deployment the site does not
30
+ live at the domain root. `linkAttrs` for the same reason too — one
31
+ list of links in two components, and a `nav` entry moved down here
32
+ must not silently change how it opens. */}
33
+ {
34
+ config.footerLinks.length > 0 && (
35
+ <p class="footer-links">
36
+ {config.footerLinks.map((link) => (
37
+ <a href={withBase(link.href)} {...linkAttrs(link)}>
38
+ {link.label}
39
+ </a>
40
+ ))}
41
+ </p>
42
+ )
43
+ }
44
+ {/* Opt-out, not opt-in: an index runner who does not want to say where the
45
+ software came from sets `attribution: false`, and this disappears.
46
+ Nothing else on the page changes.
47
+
48
+ "using", not "by": the index runner built this site — Grimoire is the
49
+ tool they built it with. */}
50
+ {
51
+ config.attribution && (
52
+ <p class="attribution">
53
+ Built with <span aria-hidden="true">♥</span><span class="sr-only">love</span> using{" "}
54
+ {/* New tab: this is the one link that leaves the index for an
55
+ unrelated site, and a visitor mid-browse should not lose their
56
+ place to it. */}
57
+ <a href="https://grimoire.rs" target="_blank" rel="noopener">
58
+ Grimoire
59
+ </a>
60
+ </p>
61
+ )
62
+ }
63
+ </div>
64
+ </footer>
@@ -0,0 +1,74 @@
1
+ ---
2
+ // The site header: brand, nav, theme toggle.
3
+ //
4
+ // Its own file so an index can replace the header ALONE — `theme/components/
5
+ // SiteHeader.astro` wins over this, and `Base.astro` still supplies the head,
6
+ // the centered `main`, the copy toast and every default style. Replacing the
7
+ // layout instead means owning all of that.
8
+ //
9
+ // It takes no props deliberately: everything it draws is build-time config,
10
+ // which it reads the same way any page does. A replacement therefore has no
11
+ // prop contract to match — it imports `data` and renders whatever it likes.
12
+ //
13
+ // The styles stay in `Base.astro`'s global block, which is `is:global` and so
14
+ // reaches this markup wherever it lives. Nothing here to keep in step.
15
+ import { Moon, Sun } from "lucide-preact";
16
+ import { linkAttrs, withBase } from "../lib/base";
17
+ import { data } from "../lib/data";
18
+
19
+ const { config } = data;
20
+ // The mark is styled separately, so it only applies when it really is the
21
+ // leading run of the brand — otherwise the header renders the plain text.
22
+ const brandRest =
23
+ config.brandMark && config.brand.startsWith(config.brandMark)
24
+ ? config.brand.slice(config.brandMark.length)
25
+ : null;
26
+ // `config.nav` arrives resolved: `resolveConfig` synthesized the docs/forge
27
+ // pair for an index that configured no nav, and named the forge host itself.
28
+ ---
29
+
30
+ <header data-slot="site-header">
31
+ <div class="shell header-row">
32
+ <a class="brand" href={withBase("/")} data-slot="brand">
33
+ {/* Decorative: the brand text is right beside it, so announcing the
34
+ logo too would read the index's name twice. */}
35
+ {config.logo && <img class="brand-logo" src={withBase(config.logo)} alt="" />}
36
+ {
37
+ brandRest === null ? (
38
+ config.brand
39
+ ) : (
40
+ <Fragment>
41
+ <span class="brand-mark">{config.brandMark}</span>
42
+ {brandRest}
43
+ </Fragment>
44
+ )
45
+ }
46
+ </a>
47
+ <nav>
48
+ {/* Two independent questions, and they used to be one branch. Every
49
+ href gets `withBase`: a page added under `theme/pages/` is linked
50
+ as `/setup/`, on project Pages the site does not live at the domain
51
+ root, and `withBase` already leaves an absolute URL alone — so
52
+ there is nothing here to guard. Whether the link opens in a new tab
53
+ is `linkAttrs`, which reads `external` and falls back to the href's
54
+ shape. An internal link must not open a tab, or browsing the
55
+ index's own guide pages litters the visitor's. */}
56
+ {
57
+ config.nav.map((link) => (
58
+ <a href={withBase(link.href)} {...linkAttrs(link)}>
59
+ {link.label}
60
+ </a>
61
+ ))
62
+ }
63
+ {/* Stroked, not filled, and inheriting the nav's own colour, so the
64
+ toggle reads as a third nav item rather than a control bolted beside
65
+ them. Rendered statically — no client directive, so no JavaScript
66
+ ships for the icons. The click handler lives in `Base.astro`, with
67
+ the rest of the document's scripts. */}
68
+ <button id="theme-toggle" type="button" aria-label="Toggle theme">
69
+ <Moon class="when-light" size={16} aria-hidden="true" />
70
+ <Sun class="when-dark" size={16} aria-hidden="true" />
71
+ </button>
72
+ </nav>
73
+ </div>
74
+ </header>
@@ -92,14 +92,14 @@ const commands: [string, string][] = [
92
92
  padding: var(--grim-space-2);
93
93
  background: var(--grim-color-card);
94
94
  border: var(--grim-border-width) solid var(--grim-color-border);
95
- border-radius: var(--grim-radius-lg);
95
+ border-radius: var(--grim-radius-control);
96
96
  box-shadow: var(--grim-shadow-raised);
97
97
  }
98
98
  .cmd {
99
99
  display: block;
100
100
  width: 100%;
101
101
  border: 0;
102
- border-radius: var(--grim-radius-md);
102
+ border-radius: var(--grim-radius-inset);
103
103
  background: transparent;
104
104
  color: var(--grim-color-muted);
105
105
  font: inherit;