@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.
- package/CHANGELOG.md +190 -0
- package/NOTICE +30 -0
- package/README.md +76 -331
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +35 -4
- package/dist/cli/init.js.map +1 -1
- package/dist/config.d.ts +107 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +181 -36
- package/dist/config.js.map +1 -1
- package/dist/renderer/astro/components/CardLogo.d.ts +5 -0
- package/dist/renderer/astro/components/CardLogo.js +58 -0
- package/dist/renderer/astro/components/CardLogo.tsx +96 -0
- package/dist/renderer/astro/components/Catalog.d.ts +14 -1
- package/dist/renderer/astro/components/Catalog.js +467 -108
- package/dist/renderer/astro/components/Catalog.tsx +756 -349
- package/dist/renderer/astro/components/CommandBar.astro +66 -0
- package/dist/renderer/astro/components/CopyButton.d.ts +7 -0
- package/dist/renderer/astro/components/CopyButton.js +28 -0
- package/dist/renderer/astro/components/CopyButton.tsx +56 -0
- package/dist/renderer/astro/components/KindMark.d.ts +69 -0
- package/dist/renderer/astro/components/KindMark.js +66 -0
- package/dist/renderer/astro/components/KindMark.tsx +141 -0
- package/dist/renderer/astro/components/PackageCard.d.ts +18 -0
- package/dist/renderer/astro/components/PackageCard.js +50 -0
- package/dist/renderer/astro/components/PackageCard.tsx +273 -0
- package/dist/renderer/astro/components/PackageRow.d.ts +10 -0
- package/dist/renderer/astro/components/PackageRow.js +32 -0
- package/dist/renderer/astro/components/PackageRow.tsx +126 -0
- package/dist/renderer/astro/components/PickerMenu.astro +5 -14
- package/dist/renderer/astro/components/SiteFooter.astro +64 -0
- package/dist/renderer/astro/components/SiteHeader.astro +74 -0
- package/dist/renderer/astro/components/VersionMenu.astro +2 -2
- package/dist/renderer/astro/layouts/Base.astro +860 -206
- package/dist/renderer/astro/lib/base.d.ts +25 -0
- package/dist/renderer/astro/lib/base.js +23 -0
- package/dist/renderer/astro/lib/base.ts +27 -0
- package/dist/renderer/astro/lib/catalog.d.ts +24 -0
- package/dist/renderer/astro/lib/catalog.js +36 -0
- package/dist/renderer/astro/lib/catalog.ts +37 -0
- package/dist/renderer/astro/lib/commands.d.ts +58 -0
- package/dist/renderer/astro/lib/commands.js +86 -0
- package/dist/renderer/astro/lib/commands.ts +117 -0
- package/dist/renderer/astro/lib/keywordRail.d.ts +44 -0
- package/dist/renderer/astro/lib/keywordRail.js +99 -0
- package/dist/renderer/astro/lib/keywordRail.ts +110 -0
- package/dist/renderer/astro/pages/index.astro +40 -87
- package/dist/renderer/astro/pages/p/[...slug].astro +340 -195
- package/dist/renderer/astro/styles/tokens.css +40 -5
- package/dist/renderer/index.d.ts +58 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +547 -5
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/types.d.ts +9 -0
- package/dist/renderer/types.d.ts.map +1 -1
- package/package.json +9 -4
- package/templates/README.md +6 -0
- package/templates/gitignore +4 -1
- package/templates/theme/README.md +38 -0
- 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
|
-
// `
|
|
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-
|
|
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-
|
|
102
|
+
border-radius: var(--grim-radius-inset);
|
|
103
103
|
background: transparent;
|
|
104
104
|
color: var(--grim-color-muted);
|
|
105
105
|
font: inherit;
|