@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.
- package/CHANGELOG.md +270 -0
- package/NOTICE +30 -0
- package/README.md +76 -331
- package/dist/cli/dev.d.ts +14 -0
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +22 -1
- package/dist/cli/dev.js.map +1 -1
- 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/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.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 +573 -108
- package/dist/renderer/astro/components/Catalog.tsx +903 -349
- package/dist/renderer/astro/components/CodeBlock.astro +74 -0
- 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 +970 -227
- 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/code.d.ts +2 -2
- package/dist/renderer/astro/lib/code.js +2 -2
- package/dist/renderer/astro/lib/code.ts +2 -2
- package/dist/renderer/astro/lib/commands.d.ts +64 -0
- package/dist/renderer/astro/lib/commands.js +91 -0
- package/dist/renderer/astro/lib/commands.ts +133 -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 +349 -198
- package/dist/renderer/astro/styles/tokens.css +40 -5
- package/dist/renderer/index.d.ts +77 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +565 -7
- 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
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
// The tokens themselves live in `../styles/tokens.css`, which declares the
|
|
8
8
|
// same layer. That file is the contract; this one only spends it.
|
|
9
9
|
import "../styles/tokens.css";
|
|
10
|
-
|
|
10
|
+
// Header and footer are separate components so an index can replace one of
|
|
11
|
+
// them without owning this file — the head, the centered `main`, the copy
|
|
12
|
+
// toast and the whole style block below are what it would otherwise inherit
|
|
13
|
+
// the hard way. Their styles stay here: the block is `is:global`, so it
|
|
14
|
+
// reaches their markup wherever it lives.
|
|
15
|
+
import SiteHeader from "../components/SiteHeader.astro";
|
|
16
|
+
import SiteFooter from "../components/SiteFooter.astro";
|
|
11
17
|
import { withBase } from "../lib/base";
|
|
12
18
|
import { data } from "../lib/data";
|
|
13
19
|
|
|
@@ -20,33 +26,6 @@ const { config, css } = data;
|
|
|
20
26
|
// `og:image` falls back to the logo before the favicon: a favicon is drawn to
|
|
21
27
|
// read at 16px, and a link preview is not that.
|
|
22
28
|
const { title, description, image = config.logo ?? config.favicon } = Astro.props;
|
|
23
|
-
// Shown as well as linked: on a subpath deployment the bare path is not
|
|
24
|
-
// where the file is, so the label has to carry the prefix too.
|
|
25
|
-
const allJson = withBase("/all.json");
|
|
26
|
-
// The mark is styled separately, so it only applies when it really is the
|
|
27
|
-
// leading run of the brand — otherwise the header renders the plain text.
|
|
28
|
-
const brandRest =
|
|
29
|
-
config.brandMark && config.brand.startsWith(config.brandMark)
|
|
30
|
-
? config.brand.slice(config.brandMark.length)
|
|
31
|
-
: null;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Name the forge `repoUrl` actually points at, rather than assuming one.
|
|
35
|
-
* `repoUrl` is free-form config: an index hosted on GitLab was previously
|
|
36
|
-
* labelled "GitHub" in its own header. An unrecognized host is named
|
|
37
|
-
* outright — honest, and already lowercase, which is the house style for
|
|
38
|
-
* this nav.
|
|
39
|
-
*/
|
|
40
|
-
const FORGES = ["github", "gitlab", "bitbucket", "codeberg", "sourcehut"];
|
|
41
|
-
function repoLabel(url: string): string {
|
|
42
|
-
let host: string;
|
|
43
|
-
try {
|
|
44
|
-
host = new URL(url).hostname.replace(/^www\./, "");
|
|
45
|
-
} catch {
|
|
46
|
-
return "repo";
|
|
47
|
-
}
|
|
48
|
-
return FORGES.find((forge) => host.includes(forge)) ?? host;
|
|
49
|
-
}
|
|
50
29
|
---
|
|
51
30
|
|
|
52
31
|
<html lang="en">
|
|
@@ -61,22 +40,38 @@ function repoLabel(url: string): string {
|
|
|
61
40
|
<meta property="og:image" content={withBase(image)} />
|
|
62
41
|
<meta name="twitter:card" content="summary" />
|
|
63
42
|
<script is:inline>
|
|
43
|
+
// Reading `localStorage` *throws* in a browser set to block site data —
|
|
44
|
+
// it does not return null — and everything below this point, the
|
|
45
|
+
// command-bar helpers included, lives in the same script block. One
|
|
46
|
+
// guarded reader, so a blocked store costs a preference rather than
|
|
47
|
+
// the page.
|
|
48
|
+
const pref = (key) => {
|
|
49
|
+
try {
|
|
50
|
+
return localStorage.getItem(key);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
64
56
|
// Before first paint: stored preference wins, else system scheme.
|
|
65
57
|
document.documentElement.dataset.theme =
|
|
66
|
-
|
|
58
|
+
pref("theme") ??
|
|
67
59
|
(matchMedia("(prefers-color-scheme: dark)").matches
|
|
68
60
|
? "dark"
|
|
69
61
|
: "light");
|
|
70
62
|
|
|
71
|
-
// Also before first paint: a
|
|
72
|
-
//
|
|
73
|
-
// empty search box
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// itself; the timeout is the
|
|
78
|
-
// hydrates, where a flash beats a
|
|
79
|
-
|
|
63
|
+
// Also before first paint: a deep link (a keyword chip on a package
|
|
64
|
+
// page) or a stored preference means the catalog the server rendered —
|
|
65
|
+
// every package, empty search box, name order, deprecated hidden — is
|
|
66
|
+
// the wrong one. Flag it so CSS holds the catalog back rather than
|
|
67
|
+
// showing the full list and rearranging it a moment later. The island
|
|
68
|
+
// drops the flag once it has applied the view — which is the render
|
|
69
|
+
// after hydration, never the hydrating one itself; the timeout is the
|
|
70
|
+
// failsafe for an island that never hydrates, where a flash beats a
|
|
71
|
+
// blank page.
|
|
72
|
+
const view = new URLSearchParams(location.search);
|
|
73
|
+
const arranged = ["sort", "dir", "deprecated"].some((k) => pref("grim.catalog." + k));
|
|
74
|
+
if (view.get("q") || view.get("kind") || arranged) {
|
|
80
75
|
document.documentElement.dataset.query = "";
|
|
81
76
|
setTimeout(() => delete document.documentElement.dataset.query, 3000);
|
|
82
77
|
}
|
|
@@ -123,88 +118,25 @@ function repoLabel(url: string): string {
|
|
|
123
118
|
</script>
|
|
124
119
|
</head>
|
|
125
120
|
<body>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</Fragment>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
</a>
|
|
143
|
-
<nav>
|
|
144
|
-
{config.docsUrl && <a href={config.docsUrl}>docs</a>}
|
|
145
|
-
{
|
|
146
|
-
config.repoUrl && (
|
|
147
|
-
<a href={config.repoUrl} target="_blank" rel="noopener noreferrer">
|
|
148
|
-
{repoLabel(config.repoUrl)}
|
|
149
|
-
</a>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
{/* Stroked, not filled, and inheriting the nav's own colour, so the
|
|
153
|
-
toggle reads as a third nav item rather than a control bolted
|
|
154
|
-
beside them. Rendered statically — no client directive, so no
|
|
155
|
-
JavaScript ships for the icons. */}
|
|
156
|
-
<button id="theme-toggle" type="button" aria-label="Toggle theme">
|
|
157
|
-
<Moon class="when-light" size={16} aria-hidden="true" />
|
|
158
|
-
<Sun class="when-dark" size={16} aria-hidden="true" />
|
|
159
|
-
</button>
|
|
160
|
-
</nav>
|
|
161
|
-
</div>
|
|
162
|
-
</header>
|
|
121
|
+
<SiteHeader />
|
|
122
|
+
|
|
123
|
+
{/* Whatever this index has to say on every page. Its own element rather
|
|
124
|
+
than a line inside `main`, so a consumer can make it full-bleed by
|
|
125
|
+
restyling one slot — the inner `shell` keeps it aligned with the page
|
|
126
|
+
until they do. */}
|
|
127
|
+
{
|
|
128
|
+
config.notice && (
|
|
129
|
+
<div class="notice" data-slot="site-notice">
|
|
130
|
+
<p class="shell">{config.notice}</p>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
163
134
|
|
|
164
135
|
<main class="shell">
|
|
165
136
|
<slot />
|
|
166
137
|
</main>
|
|
167
138
|
|
|
168
|
-
<
|
|
169
|
-
<div class="shell">
|
|
170
|
-
<p>
|
|
171
|
-
Raw data: <a href={allJson}><code>{allJson}</code></a>{
|
|
172
|
-
config.footerNote ? ` · ${config.footerNote}` : ""
|
|
173
|
-
}
|
|
174
|
-
</p>
|
|
175
|
-
{/* Whatever this index has to link — a privacy notice, an imprint, a
|
|
176
|
-
code of conduct. Absent by default, so a footer without them looks
|
|
177
|
-
exactly as it did before. */}
|
|
178
|
-
{
|
|
179
|
-
config.footerLinks.length > 0 && (
|
|
180
|
-
<p class="footer-links">
|
|
181
|
-
{config.footerLinks.map((link) => (
|
|
182
|
-
<a href={link.href}>{link.label}</a>
|
|
183
|
-
))}
|
|
184
|
-
</p>
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
{/* Opt-out, not opt-in: an index runner who does not want to say
|
|
188
|
-
where the software came from sets `attribution: false`, and this
|
|
189
|
-
disappears. Nothing else on the page changes.
|
|
190
|
-
|
|
191
|
-
"using", not "by": the index runner built this site — Grimoire is
|
|
192
|
-
the tool they built it with. */}
|
|
193
|
-
{
|
|
194
|
-
config.attribution && (
|
|
195
|
-
<p class="attribution">
|
|
196
|
-
Built with <span aria-hidden="true">♥</span><span class="sr-only">love</span> using{" "}
|
|
197
|
-
{/* New tab: this is the one link that leaves the index for an
|
|
198
|
-
unrelated site, and a visitor mid-browse should not lose
|
|
199
|
-
their place to it. */}
|
|
200
|
-
<a href="https://grimoire.rs" target="_blank" rel="noopener">
|
|
201
|
-
Grimoire
|
|
202
|
-
</a>
|
|
203
|
-
</p>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
</div>
|
|
207
|
-
</footer>
|
|
139
|
+
<SiteFooter />
|
|
208
140
|
|
|
209
141
|
{/* One toast for the whole page, outside every scrolling container so a
|
|
210
142
|
copy from a card at the bottom lands in the same place as one from
|
|
@@ -216,7 +148,11 @@ function repoLabel(url: string): string {
|
|
|
216
148
|
</div>
|
|
217
149
|
|
|
218
150
|
<script is:inline>
|
|
219
|
-
|
|
151
|
+
// Optional: an index repo that replaces `SiteHeader.astro` is free to
|
|
152
|
+
// ship a header with no toggle, and this block runs on every page
|
|
153
|
+
// regardless. Unguarded, that one dereference threw and took every
|
|
154
|
+
// script below it with it — copy buttons, picker wiring, toast.
|
|
155
|
+
document.getElementById("theme-toggle")?.addEventListener("click", () => {
|
|
220
156
|
const root = document.documentElement;
|
|
221
157
|
const next = root.dataset.theme === "dark" ? "light" : "dark";
|
|
222
158
|
root.dataset.theme = next;
|
|
@@ -280,6 +216,26 @@ function repoLabel(url: string): string {
|
|
|
280
216
|
}
|
|
281
217
|
}
|
|
282
218
|
|
|
219
|
+
// Preselect the host platform in every bar that asked for it. Lives
|
|
220
|
+
// here rather than in `index.astro` because `detect` has to mean the
|
|
221
|
+
// same thing wherever a bar is drawn — a page under `theme/pages/`
|
|
222
|
+
// rendering `<CommandBar detect />` gets this, and used to get the
|
|
223
|
+
// configured first choice however the visitor arrived. A platform the
|
|
224
|
+
// bar does not offer leaves that first choice alone.
|
|
225
|
+
//
|
|
226
|
+
// The landing page ALSO runs this in its own script, right beside the
|
|
227
|
+
// hero: this one sits past the whole catalog, late enough for the
|
|
228
|
+
// browser to paint a Linux command at someone on Windows first. Both
|
|
229
|
+
// runs pick the same choice and `__grimPick` is idempotent, so the
|
|
230
|
+
// duplicate costs nothing and the early one is what a visitor sees.
|
|
231
|
+
for (const bar of document.querySelectorAll("[data-os-detect]")) {
|
|
232
|
+
const host = window.__grimHostOs();
|
|
233
|
+
const match = [...bar.querySelectorAll("[data-os-pick]")].find(
|
|
234
|
+
(p) => p.dataset.osName === host,
|
|
235
|
+
);
|
|
236
|
+
if (match) window.__grimPick(bar, match);
|
|
237
|
+
}
|
|
238
|
+
|
|
283
239
|
// One toast for every copy on the page. The in-place check confirms
|
|
284
240
|
// that *something* was copied; a card has three copy buttons a few
|
|
285
241
|
// pixels apart, all of which flash the same check, so the toast is
|
|
@@ -323,10 +279,25 @@ function repoLabel(url: string): string {
|
|
|
323
279
|
'<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
324
280
|
|
|
325
281
|
for (const pre of document.querySelectorAll("pre.astro-code")) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
282
|
+
// `CodeBlock.astro` already ships the wrapper and the action slot, so
|
|
283
|
+
// adopt them where they are there. Wrapping a second time would nest
|
|
284
|
+
// two positioning contexts and orphan whatever the component put in
|
|
285
|
+
// the slot.
|
|
286
|
+
const parent = pre.parentElement;
|
|
287
|
+
let wrapper = parent?.classList.contains("code-block") ? parent : null;
|
|
288
|
+
if (!wrapper) {
|
|
289
|
+
wrapper = document.createElement("div");
|
|
290
|
+
wrapper.className = "code-block";
|
|
291
|
+
pre.replaceWith(wrapper);
|
|
292
|
+
wrapper.append(pre);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let actions = wrapper.querySelector(".code-actions");
|
|
296
|
+
if (!actions) {
|
|
297
|
+
actions = document.createElement("span");
|
|
298
|
+
actions.className = "code-actions";
|
|
299
|
+
wrapper.append(actions);
|
|
300
|
+
}
|
|
330
301
|
|
|
331
302
|
const btn = document.createElement("button");
|
|
332
303
|
btn.type = "button";
|
|
@@ -336,9 +307,10 @@ function repoLabel(url: string): string {
|
|
|
336
307
|
// `textContent`, not `innerText`: the block is not rendered yet at
|
|
337
308
|
// this point, and `innerText` would collapse its whitespace.
|
|
338
309
|
btn.dataset.copy = pre.textContent ?? "";
|
|
339
|
-
|
|
310
|
+
// A component-rendered block names itself; a markdown one cannot.
|
|
311
|
+
btn.dataset.copyName = wrapper.dataset.copyName || "code block";
|
|
340
312
|
btn.innerHTML = CLIPBOARD_ICON + '<span class="sr-only" role="status"></span>';
|
|
341
|
-
|
|
313
|
+
actions.append(btn);
|
|
342
314
|
}
|
|
343
315
|
|
|
344
316
|
for (const btn of document.querySelectorAll("[data-copy]")) {
|
|
@@ -404,6 +376,20 @@ function repoLabel(url: string): string {
|
|
|
404
376
|
padding-block: var(--grim-space-5);
|
|
405
377
|
gap: var(--grim-space-6);
|
|
406
378
|
}
|
|
379
|
+
/* Deliberately neutral, not the amber of `--grim-color-banner-*`:
|
|
380
|
+
that palette says "warning", and this line is whatever the index
|
|
381
|
+
has to state on every page. An index that wants it loud restyles
|
|
382
|
+
`[data-slot="site-notice"]` — one selector, no new token. */
|
|
383
|
+
.notice {
|
|
384
|
+
border-bottom: var(--grim-border-width) solid var(--grim-color-border);
|
|
385
|
+
background: var(--grim-color-chip-bg);
|
|
386
|
+
color: var(--grim-color-muted);
|
|
387
|
+
font-size: var(--grim-text-sm);
|
|
388
|
+
}
|
|
389
|
+
.notice p {
|
|
390
|
+
margin: 0;
|
|
391
|
+
padding-block: var(--grim-space-4);
|
|
392
|
+
}
|
|
407
393
|
.brand {
|
|
408
394
|
display: inline-flex;
|
|
409
395
|
align-items: center;
|
|
@@ -506,7 +492,7 @@ function repoLabel(url: string): string {
|
|
|
506
492
|
}
|
|
507
493
|
.hero code {
|
|
508
494
|
background: var(--grim-color-chip-bg);
|
|
509
|
-
border-radius: var(--grim-radius-
|
|
495
|
+
border-radius: var(--grim-radius-code);
|
|
510
496
|
padding: var(--grim-space-1) var(--grim-space-3);
|
|
511
497
|
/* `em`, not a type token: inline code sizes against the prose it
|
|
512
498
|
sits in, so it must follow whatever that text is set at. */
|
|
@@ -521,7 +507,7 @@ function repoLabel(url: string): string {
|
|
|
521
507
|
min-width: 0;
|
|
522
508
|
background: var(--grim-color-card);
|
|
523
509
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
524
|
-
border-radius: var(--grim-radius-
|
|
510
|
+
border-radius: var(--grim-radius-control);
|
|
525
511
|
}
|
|
526
512
|
/* No `overflow: hidden` — that would clip the platform menu. The end
|
|
527
513
|
segments round themselves instead (8px border box, 1px border).
|
|
@@ -677,7 +663,7 @@ function repoLabel(url: string): string {
|
|
|
677
663
|
list-style: none;
|
|
678
664
|
background: var(--grim-color-card);
|
|
679
665
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
680
|
-
border-radius: var(--grim-radius-
|
|
666
|
+
border-radius: var(--grim-radius-control);
|
|
681
667
|
box-shadow: var(--grim-shadow-raised);
|
|
682
668
|
}
|
|
683
669
|
.os-menu li button {
|
|
@@ -686,7 +672,7 @@ function repoLabel(url: string): string {
|
|
|
686
672
|
gap: var(--grim-space-4);
|
|
687
673
|
width: 100%;
|
|
688
674
|
border: none;
|
|
689
|
-
border-radius: var(--grim-radius-
|
|
675
|
+
border-radius: var(--grim-radius-inset);
|
|
690
676
|
background: none;
|
|
691
677
|
padding: var(--grim-space-3) var(--grim-space-4);
|
|
692
678
|
color: var(--grim-color-fg);
|
|
@@ -718,14 +704,19 @@ function repoLabel(url: string): string {
|
|
|
718
704
|
shows until a hover, focus or open state paints a background — then
|
|
719
705
|
square corners poke out past the bar's own 8px rounding.
|
|
720
706
|
The summary is named explicitly rather than left to inherit. */
|
|
707
|
+
/* Clamped at zero, because the radius is a knob now: with
|
|
708
|
+
`--grim-radius-base: 0px` the bare subtraction is `-1px`, and a
|
|
709
|
+
negative `border-radius` is not invalid-but-ignored per corner —
|
|
710
|
+
it drops the whole declaration. Harmless while everything is
|
|
711
|
+
square, and a trap the moment someone rounds the site back up. */
|
|
721
712
|
.cmd-bar > :first-child,
|
|
722
713
|
.cmd-bar > :first-child > summary {
|
|
723
|
-
border-start-start-radius: calc(var(--grim-radius-
|
|
724
|
-
border-end-start-radius: calc(var(--grim-radius-
|
|
714
|
+
border-start-start-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
715
|
+
border-end-start-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
725
716
|
}
|
|
726
717
|
.cmd-bar > :last-child {
|
|
727
|
-
border-start-end-radius: calc(var(--grim-radius-
|
|
728
|
-
border-end-end-radius: calc(var(--grim-radius-
|
|
718
|
+
border-start-end-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
719
|
+
border-end-end-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
729
720
|
}
|
|
730
721
|
|
|
731
722
|
/* Attribution sits opposite the raw-data line, on the same row where
|
|
@@ -770,7 +761,7 @@ function repoLabel(url: string): string {
|
|
|
770
761
|
padding: var(--grim-space-4) var(--grim-space-5);
|
|
771
762
|
background: var(--grim-color-card);
|
|
772
763
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
773
|
-
border-radius: var(--grim-radius-
|
|
764
|
+
border-radius: var(--grim-radius-surface);
|
|
774
765
|
box-shadow: var(--grim-shadow-overlay);
|
|
775
766
|
/* Out of the layout and unhittable until shown, so it never eats a
|
|
776
767
|
click aimed at whatever is beneath it. */
|
|
@@ -817,22 +808,30 @@ function repoLabel(url: string): string {
|
|
|
817
808
|
visibility: hidden;
|
|
818
809
|
}
|
|
819
810
|
|
|
811
|
+
/* Three stacked rows, not one wrapping line: the search field, then
|
|
812
|
+
the filters, then what they left plus the controls that arrange it.
|
|
813
|
+
A wrapping row put a different set on each line at each viewport
|
|
814
|
+
width, so the toolbar read as a different object on every screen. */
|
|
820
815
|
.controls {
|
|
821
816
|
display: flex;
|
|
822
|
-
flex-
|
|
823
|
-
align-items:
|
|
817
|
+
flex-direction: column;
|
|
818
|
+
align-items: stretch;
|
|
824
819
|
gap: var(--grim-space-5);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
820
|
+
/* Close, because the last row of the toolbar belongs to the list
|
|
821
|
+
below rather than to the toolbar above it — it says what the
|
|
822
|
+
filters left and how it is arranged. Proximity is what groups
|
|
823
|
+
them; `.meta-row` opens the gap on its other side to match. */
|
|
824
|
+
margin-bottom: var(--grim-space-3);
|
|
825
|
+
}
|
|
826
|
+
/* Wide basis, not full width: the field and the sort control share
|
|
827
|
+
the first line and the kind chips wrap to the next, and on a narrow
|
|
828
|
+
viewport the sort control drops below the field instead of
|
|
829
|
+
squeezing it. The container gap separates the rows and reads wider
|
|
830
|
+
than the gap inside a chip group, so the groups still read as
|
|
831
|
+
groups without a wrapper element. */
|
|
832
832
|
.search-field {
|
|
833
833
|
position: relative;
|
|
834
834
|
display: flex;
|
|
835
|
-
flex: 1 1 100%;
|
|
836
835
|
/* Breathing room above the field when `/` scrolls it to the top of
|
|
837
836
|
the viewport, so it does not sit flush against the edge. */
|
|
838
837
|
scroll-margin-top: 1.25rem;
|
|
@@ -844,7 +843,7 @@ function repoLabel(url: string): string {
|
|
|
844
843
|
/* 2.4rem is the hint's measured width, not a rhythm step. */
|
|
845
844
|
padding-right: 2.4rem;
|
|
846
845
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
847
|
-
border-radius: var(--grim-radius-
|
|
846
|
+
border-radius: var(--grim-radius-control);
|
|
848
847
|
background: var(--grim-color-card);
|
|
849
848
|
color: var(--grim-color-fg);
|
|
850
849
|
font-size: var(--grim-text-md);
|
|
@@ -866,7 +865,7 @@ function repoLabel(url: string): string {
|
|
|
866
865
|
pointer-events: none;
|
|
867
866
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
868
867
|
border-bottom-width: 2px;
|
|
869
|
-
border-radius: var(--grim-radius-
|
|
868
|
+
border-radius: var(--grim-radius-inset);
|
|
870
869
|
background: var(--grim-color-chip-bg);
|
|
871
870
|
color: var(--grim-color-muted);
|
|
872
871
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
@@ -878,24 +877,99 @@ function repoLabel(url: string): string {
|
|
|
878
877
|
.search-field:has(input:not(:placeholder-shown)) .search-hint {
|
|
879
878
|
display: none;
|
|
880
879
|
}
|
|
880
|
+
/* One line, always. Wrapping was the defect: at a narrow width the
|
|
881
|
+
row dropped `deprecated` onto a second line, because no chip can
|
|
882
|
+
shrink below its own longest word and the rail's chip count is a
|
|
883
|
+
constant rather than a viewport reading. So the KEYWORD group is
|
|
884
|
+
the single flexible child and scrolls; the kinds and the toggle
|
|
885
|
+
keep their size and their place. */
|
|
886
|
+
.filter-row {
|
|
887
|
+
display: flex;
|
|
888
|
+
align-items: center;
|
|
889
|
+
gap: var(--grim-space-3);
|
|
890
|
+
flex-wrap: nowrap;
|
|
891
|
+
/* Backstop for the extreme case where the kinds alone overflow. */
|
|
892
|
+
overflow-x: auto;
|
|
893
|
+
}
|
|
881
894
|
.chips {
|
|
882
895
|
display: flex;
|
|
883
|
-
|
|
896
|
+
align-items: center;
|
|
884
897
|
gap: var(--grim-space-3);
|
|
885
898
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
899
|
+
.chips.kind-chips {
|
|
900
|
+
flex-shrink: 0;
|
|
901
|
+
}
|
|
902
|
+
/* The one child that absorbs the row's slack — which is also what
|
|
903
|
+
pushes the overflow menu and the deprecated toggle to the far end,
|
|
904
|
+
without an `auto` margin that would put them out of reach once the
|
|
905
|
+
row does overflow.
|
|
906
|
+
|
|
907
|
+
`hidden`, never `auto`: a scrollbar here put the chips behind a
|
|
908
|
+
gesture nobody looks for and, worse, let the rail grow past the row
|
|
909
|
+
so the overflow menu it was supposed to sit beside was pushed off
|
|
910
|
+
the end entirely. What does not fit is decided by measurement (see
|
|
911
|
+
`railFit`) and drawn as nothing. */
|
|
912
|
+
.chips.kw-rail {
|
|
913
|
+
flex: 1 1 auto;
|
|
914
|
+
min-width: 0;
|
|
915
|
+
overflow: hidden;
|
|
916
|
+
}
|
|
917
|
+
/* Past the measured fit. `visibility`, not `display`: the chip keeps
|
|
918
|
+
its box, so the measurement that excluded it stays true instead of
|
|
919
|
+
freeing the width that would let it back in. It is in the overflow
|
|
920
|
+
menu, which is where the reader reaches it. */
|
|
921
|
+
.chip.kw.clipped {
|
|
922
|
+
visibility: hidden;
|
|
923
|
+
}
|
|
924
|
+
.filter-divider {
|
|
925
|
+
flex-shrink: 0;
|
|
890
926
|
align-self: stretch;
|
|
891
|
-
width:
|
|
927
|
+
width: var(--grim-border-width);
|
|
892
928
|
background: var(--grim-color-border);
|
|
893
929
|
}
|
|
894
|
-
/*
|
|
895
|
-
|
|
896
|
-
|
|
930
|
+
/* Answers a different question from sort and kind — what the catalog
|
|
931
|
+
is withholding, not how to arrange it — so it sits apart, at the
|
|
932
|
+
end of the row. */
|
|
897
933
|
.chip.deprecated-toggle {
|
|
898
|
-
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
/* Lifts every keyword at once, and is only in the markup while there
|
|
937
|
+
is one to lift — so it never competes with the rail for room in the
|
|
938
|
+
common case. Muted until hovered: it undoes work, and it should not
|
|
939
|
+
read as another facet to pick. */
|
|
940
|
+
.chip.kw-clear {
|
|
941
|
+
flex-shrink: 0;
|
|
942
|
+
gap: var(--grim-space-2);
|
|
943
|
+
color: var(--grim-color-muted);
|
|
944
|
+
}
|
|
945
|
+
.chip.kw-clear:hover,
|
|
946
|
+
.chip.kw-clear:focus-visible {
|
|
947
|
+
border-color: var(--grim-color-accent);
|
|
948
|
+
color: var(--grim-color-fg);
|
|
949
|
+
}
|
|
950
|
+
.kw-menu {
|
|
951
|
+
flex-shrink: 0;
|
|
952
|
+
}
|
|
953
|
+
/* Row three: the answer on the left, the controls that arrange it on
|
|
954
|
+
the right. */
|
|
955
|
+
.meta-row {
|
|
956
|
+
display: flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
gap: var(--grim-space-4);
|
|
959
|
+
flex-wrap: wrap;
|
|
960
|
+
/* On top of the stack's own gap: it takes this row away from the
|
|
961
|
+
filters and leaves it sitting on the list it describes. */
|
|
962
|
+
margin-top: var(--grim-space-5);
|
|
963
|
+
}
|
|
964
|
+
/* Bottom-aligned, not centred: it is a caption for the list below
|
|
965
|
+
it, and centring it against two bordered controls left it floating
|
|
966
|
+
between the two rows belonging to neither. */
|
|
967
|
+
.result-count {
|
|
968
|
+
margin: 0;
|
|
969
|
+
margin-right: auto;
|
|
970
|
+
align-self: end;
|
|
971
|
+
color: var(--grim-color-muted);
|
|
972
|
+
font-size: var(--grim-text-sm);
|
|
899
973
|
}
|
|
900
974
|
/* Pressed state borrows the deprecation colour rather than the
|
|
901
975
|
accent, so "deprecated entries are in view" is signalled by the
|
|
@@ -905,20 +979,81 @@ function repoLabel(url: string): string {
|
|
|
905
979
|
border-color: var(--grim-color-deprecated);
|
|
906
980
|
background: var(--grim-color-chip-bg);
|
|
907
981
|
}
|
|
982
|
+
/* One object with two halves — a direction cap and the field —
|
|
983
|
+
sharing an edge, and built to the search field's own measurements
|
|
984
|
+
so the pair sits level with it at the same height. Squarer than a
|
|
985
|
+
chip on purpose: a chip toggles a filter, this chooses a value,
|
|
986
|
+
and it belongs to the field beside it rather than to the row of
|
|
987
|
+
filters below. */
|
|
988
|
+
.sort-group {
|
|
989
|
+
display: flex;
|
|
990
|
+
align-items: stretch;
|
|
991
|
+
flex: 0 0 auto;
|
|
992
|
+
}
|
|
993
|
+
/* Sized to their own row, not to the search field. They used to sit
|
|
994
|
+
beside it and had to match its height to read as one strip; on a
|
|
995
|
+
line of their own that height is just bulk. */
|
|
996
|
+
.sort-dir,
|
|
997
|
+
.sort-field {
|
|
998
|
+
position: relative;
|
|
999
|
+
padding: var(--grim-space-2) var(--grim-space-4);
|
|
1000
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1001
|
+
background: var(--grim-color-card);
|
|
1002
|
+
color: var(--grim-color-fg);
|
|
1003
|
+
font-family: inherit;
|
|
1004
|
+
font-size: var(--grim-text-sm);
|
|
1005
|
+
cursor: pointer;
|
|
1006
|
+
}
|
|
1007
|
+
.sort-dir {
|
|
1008
|
+
display: inline-flex;
|
|
1009
|
+
align-items: center;
|
|
1010
|
+
border-radius: var(--grim-radius-control) 0 0 var(--grim-radius-control);
|
|
1011
|
+
}
|
|
1012
|
+
/* Pulled onto its neighbour's border so the seam is one line, not a
|
|
1013
|
+
doubled one. Derived from the token, never a hand-written -1px. */
|
|
1014
|
+
.sort-field {
|
|
1015
|
+
border-radius: 0 var(--grim-radius-control) var(--grim-radius-control) 0;
|
|
1016
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
1017
|
+
}
|
|
1018
|
+
.sort-dir:hover,
|
|
1019
|
+
.sort-field:hover {
|
|
1020
|
+
border-color: var(--grim-color-accent);
|
|
1021
|
+
}
|
|
1022
|
+
/* Whichever half is hovered or focused owns the shared edge. */
|
|
1023
|
+
.sort-dir:hover,
|
|
1024
|
+
.sort-field:hover,
|
|
1025
|
+
.sort-dir:focus-visible,
|
|
1026
|
+
.sort-field:focus-visible {
|
|
1027
|
+
z-index: 1;
|
|
1028
|
+
}
|
|
1029
|
+
.sort-dir:focus-visible,
|
|
1030
|
+
.sort-field:focus-visible {
|
|
1031
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1032
|
+
outline-offset: 1px;
|
|
1033
|
+
}
|
|
1034
|
+
/* One box for every chip in the filter row — the kinds, the keywords,
|
|
1035
|
+
the deprecated toggle and the overflow menu's trigger alike. The
|
|
1036
|
+
line-height is stated rather than inherited because a `<summary>`
|
|
1037
|
+
and a `<button>` do not agree on their own, and a row of chips that
|
|
1038
|
+
disagree by a pixel reads as sloppy rather than as a group. */
|
|
908
1039
|
.chip {
|
|
1040
|
+
display: inline-flex;
|
|
1041
|
+
align-items: center;
|
|
909
1042
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
910
1043
|
background: var(--grim-color-chip-bg);
|
|
911
1044
|
color: var(--grim-color-fg);
|
|
912
1045
|
border-radius: var(--grim-radius-pill);
|
|
913
1046
|
padding: var(--grim-space-2) var(--grim-space-5);
|
|
914
1047
|
font-size: var(--grim-text-sm);
|
|
1048
|
+
line-height: 1.4;
|
|
1049
|
+
white-space: nowrap;
|
|
915
1050
|
transition: border-color var(--grim-duration-base) ease;
|
|
916
1051
|
cursor: pointer;
|
|
917
1052
|
}
|
|
918
1053
|
.chip small {
|
|
919
1054
|
color: var(--grim-color-muted);
|
|
920
1055
|
}
|
|
921
|
-
/* button-only:
|
|
1056
|
+
/* button-only: a chip rendered as a span is inert and takes no hover */
|
|
922
1057
|
button.chip:hover {
|
|
923
1058
|
border-color: var(--grim-color-accent);
|
|
924
1059
|
}
|
|
@@ -926,6 +1061,189 @@ function repoLabel(url: string): string {
|
|
|
926
1061
|
border-color: var(--grim-color-accent);
|
|
927
1062
|
background: var(--grim-color-accent-soft);
|
|
928
1063
|
}
|
|
1064
|
+
/* The keyword rail reorders on every click — it is rescored against
|
|
1065
|
+
what is on screen — and the chips slide rather than jump. The FLIP
|
|
1066
|
+
effect in `Catalog.tsx` sets an inline `translate` back to where
|
|
1067
|
+
each chip was and drops it on the next frame; this transition is
|
|
1068
|
+
what plays that back. Without a declared duration the invert would
|
|
1069
|
+
simply stick. */
|
|
1070
|
+
.chip.kw {
|
|
1071
|
+
transition:
|
|
1072
|
+
border-color var(--grim-duration-base) ease,
|
|
1073
|
+
translate var(--grim-duration-slow) ease-out;
|
|
1074
|
+
}
|
|
1075
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1076
|
+
.chip.kw {
|
|
1077
|
+
transition: border-color var(--grim-duration-base) ease;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/* The overflow menu. A popover, and it has to be one: `.filter-row`
|
|
1081
|
+
above is a scroll container, and an absolutely-positioned panel
|
|
1082
|
+
inside a scroll container is cropped to the row AND stretches the
|
|
1083
|
+
row's scroll extent — which is where a menu that opened invisibly,
|
|
1084
|
+
with a stray horizontal and vertical scrollbar to show for it, came
|
|
1085
|
+
from. The top layer is outside every ancestor's `overflow`, and it
|
|
1086
|
+
brings light dismiss and Escape, which the `<details>` this replaced
|
|
1087
|
+
never had. Its seat is `placeKwMenu` in `Catalog.tsx`. */
|
|
1088
|
+
.kw-menu {
|
|
1089
|
+
display: flex;
|
|
1090
|
+
}
|
|
1091
|
+
/* The trigger wears `.chip`, so its box comes from there and cannot
|
|
1092
|
+
drift a pixel off the chips it sits beside. Only what a chip does
|
|
1093
|
+
not cover is stated here. */
|
|
1094
|
+
.kw-menu > button {
|
|
1095
|
+
display: inline-flex;
|
|
1096
|
+
align-items: center;
|
|
1097
|
+
color: var(--grim-color-muted);
|
|
1098
|
+
}
|
|
1099
|
+
.kw-menu > button:hover,
|
|
1100
|
+
.kw-menu > button:focus-visible,
|
|
1101
|
+
.kw-menu > button[aria-expanded="true"] {
|
|
1102
|
+
border-color: var(--grim-color-accent);
|
|
1103
|
+
color: var(--grim-color-fg);
|
|
1104
|
+
}
|
|
1105
|
+
/* `inset` and `margin` undo the UA's centring of a popover, so the
|
|
1106
|
+
`left`/`top` the script writes are the whole position. `color` is
|
|
1107
|
+
restated because the UA sets `CanvasText` on `[popover]` directly,
|
|
1108
|
+
which beats what the panel would otherwise inherit. `display` is
|
|
1109
|
+
deliberately absent: the UA's `display: none` for a closed popover
|
|
1110
|
+
has to keep winning, so the flex box is declared on `:popover-open`
|
|
1111
|
+
alone — an unconditional `display` here would show the menu shut. */
|
|
1112
|
+
.kw-menu-panel {
|
|
1113
|
+
position: fixed;
|
|
1114
|
+
inset: auto;
|
|
1115
|
+
margin: 0;
|
|
1116
|
+
/* The panel's own measurement: wide enough for the longest keyword
|
|
1117
|
+
a publisher is likely to write plus its count, and capped so a
|
|
1118
|
+
long vocabulary scrolls instead of running off the viewport. */
|
|
1119
|
+
width: 15rem;
|
|
1120
|
+
padding: var(--grim-space-3);
|
|
1121
|
+
background: var(--grim-color-card);
|
|
1122
|
+
color: var(--grim-color-fg);
|
|
1123
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1124
|
+
border-radius: var(--grim-radius-control);
|
|
1125
|
+
box-shadow: var(--grim-shadow-raised);
|
|
1126
|
+
/* The list inside is the thing that scrolls. Without this the UA's
|
|
1127
|
+
own `overflow: auto` on `[popover]` adds a second scrollbar once
|
|
1128
|
+
the script caps the panel's height. */
|
|
1129
|
+
overflow: hidden;
|
|
1130
|
+
}
|
|
1131
|
+
.kw-menu-panel:popover-open {
|
|
1132
|
+
display: flex;
|
|
1133
|
+
flex-direction: column;
|
|
1134
|
+
gap: var(--grim-space-3);
|
|
1135
|
+
}
|
|
1136
|
+
.kw-menu-search {
|
|
1137
|
+
width: 100%;
|
|
1138
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1139
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1140
|
+
border-radius: var(--grim-radius-inset);
|
|
1141
|
+
background: var(--grim-color-bg);
|
|
1142
|
+
color: var(--grim-color-fg);
|
|
1143
|
+
font: inherit;
|
|
1144
|
+
font-size: var(--grim-text-sm);
|
|
1145
|
+
}
|
|
1146
|
+
.kw-menu-search:focus-visible {
|
|
1147
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1148
|
+
outline-offset: 1px;
|
|
1149
|
+
}
|
|
1150
|
+
.kw-menu-list {
|
|
1151
|
+
display: flex;
|
|
1152
|
+
flex-direction: column;
|
|
1153
|
+
/* Ten rows and then a scrollbar — a measured height, not a rhythm
|
|
1154
|
+
step: it is the row's own height times the count that still reads
|
|
1155
|
+
as a list rather than a page. */
|
|
1156
|
+
max-height: 15rem;
|
|
1157
|
+
overflow-y: auto;
|
|
1158
|
+
/* And shorter still where the panel's own cap is tighter — a
|
|
1159
|
+
flex item's automatic minimum size is its content, so without
|
|
1160
|
+
`min-height` the list refuses to shrink and overflows the panel
|
|
1161
|
+
the script just sized to the space below the trigger. */
|
|
1162
|
+
min-height: 0;
|
|
1163
|
+
}
|
|
1164
|
+
.kw-menu-item {
|
|
1165
|
+
display: flex;
|
|
1166
|
+
align-items: center;
|
|
1167
|
+
justify-content: space-between;
|
|
1168
|
+
gap: var(--grim-space-4);
|
|
1169
|
+
width: 100%;
|
|
1170
|
+
border: none;
|
|
1171
|
+
border-radius: var(--grim-radius-inset);
|
|
1172
|
+
background: none;
|
|
1173
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1174
|
+
color: var(--grim-color-fg);
|
|
1175
|
+
font: inherit;
|
|
1176
|
+
font-size: var(--grim-text-sm);
|
|
1177
|
+
text-align: left;
|
|
1178
|
+
cursor: pointer;
|
|
1179
|
+
}
|
|
1180
|
+
.kw-menu-item:hover,
|
|
1181
|
+
.kw-menu-item:focus-visible {
|
|
1182
|
+
background: var(--grim-color-chip-bg);
|
|
1183
|
+
}
|
|
1184
|
+
.kw-menu-item small {
|
|
1185
|
+
color: var(--grim-color-muted);
|
|
1186
|
+
}
|
|
1187
|
+
.kw-menu-empty {
|
|
1188
|
+
margin: 0;
|
|
1189
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1190
|
+
color: var(--grim-color-muted);
|
|
1191
|
+
font-size: var(--grim-text-sm);
|
|
1192
|
+
}
|
|
1193
|
+
/* Cards or list, beside the sort control because it answers the same
|
|
1194
|
+
question — how the catalog is arranged. Built to the sort group's
|
|
1195
|
+
measurements, and the second button is pulled onto the first's
|
|
1196
|
+
border so the seam is one line rather than a doubled one. */
|
|
1197
|
+
.view-toggle {
|
|
1198
|
+
display: flex;
|
|
1199
|
+
align-items: stretch;
|
|
1200
|
+
flex: 0 0 auto;
|
|
1201
|
+
/* The row centres its items, which left these two shorter than the
|
|
1202
|
+
search field and the sort control beside them: a flex container
|
|
1203
|
+
raises no strut, so a button holding nothing but a 15px icon is
|
|
1204
|
+
15px tall plus its padding, while the select next to it is a text
|
|
1205
|
+
line tall. Stretching to the line's own height is what makes the
|
|
1206
|
+
three agree without any of them stating a height — `.sort-group`
|
|
1207
|
+
gets the same result from its own `align-items: stretch`, since
|
|
1208
|
+
the select inside it is the tall one. */
|
|
1209
|
+
align-self: stretch;
|
|
1210
|
+
}
|
|
1211
|
+
.view-pick {
|
|
1212
|
+
position: relative;
|
|
1213
|
+
display: inline-flex;
|
|
1214
|
+
align-items: center;
|
|
1215
|
+
justify-content: center;
|
|
1216
|
+
padding: var(--grim-space-2) var(--grim-space-4);
|
|
1217
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1218
|
+
background: var(--grim-color-card);
|
|
1219
|
+
color: var(--grim-color-muted);
|
|
1220
|
+
cursor: pointer;
|
|
1221
|
+
}
|
|
1222
|
+
.view-pick:first-child {
|
|
1223
|
+
border-radius: var(--grim-radius-control) 0 0 var(--grim-radius-control);
|
|
1224
|
+
}
|
|
1225
|
+
.view-pick:last-child {
|
|
1226
|
+
border-radius: 0 var(--grim-radius-control) var(--grim-radius-control) 0;
|
|
1227
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
1228
|
+
}
|
|
1229
|
+
.view-pick:hover {
|
|
1230
|
+
border-color: var(--grim-color-accent);
|
|
1231
|
+
}
|
|
1232
|
+
.view-pick.active {
|
|
1233
|
+
border-color: var(--grim-color-accent);
|
|
1234
|
+
background: var(--grim-color-accent-soft);
|
|
1235
|
+
color: var(--grim-color-fg);
|
|
1236
|
+
}
|
|
1237
|
+
/* Whichever half is hovered, focused or current owns the shared edge. */
|
|
1238
|
+
.view-pick:hover,
|
|
1239
|
+
.view-pick:focus-visible,
|
|
1240
|
+
.view-pick.active {
|
|
1241
|
+
z-index: 1;
|
|
1242
|
+
}
|
|
1243
|
+
.view-pick:focus-visible {
|
|
1244
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1245
|
+
outline-offset: 1px;
|
|
1246
|
+
}
|
|
929
1247
|
|
|
930
1248
|
.grid {
|
|
931
1249
|
list-style: none;
|
|
@@ -935,14 +1253,41 @@ function repoLabel(url: string): string {
|
|
|
935
1253
|
grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
|
|
936
1254
|
gap: var(--grim-space-6);
|
|
937
1255
|
}
|
|
1256
|
+
/* Off-screen cards skip layout and paint until they are scrolled to.
|
|
1257
|
+
A card is the expensive item on this page — a masked keyword strip,
|
|
1258
|
+
a clipped watermark in its own stacking context, a logo — and the
|
|
1259
|
+
grid and the table are separate component trees, so switching view
|
|
1260
|
+
repaints every one of them at once.
|
|
1261
|
+
|
|
1262
|
+
`auto` remembers each card's last real height, so the scrollbar
|
|
1263
|
+
stops jumping after the first pass; the `18rem` is only the guess
|
|
1264
|
+
before that. Cards stay focusable and findable — the engine renders
|
|
1265
|
+
one on focus or find-in-page.
|
|
1266
|
+
|
|
1267
|
+
ponytail: this bounds the PAINT, not the JS. Measured in jsdom (no
|
|
1268
|
+
layout at all) the Preact commit for one view switch is ~30ms at 100
|
|
1269
|
+
packages, ~90-170ms at 500 and ~180-350ms at 1000, because the two
|
|
1270
|
+
views share no element types and every card unmounts as every row
|
|
1271
|
+
mounts. Past a few hundred packages the answer is windowing the
|
|
1272
|
+
list, not another CSS property. */
|
|
1273
|
+
.grid > li {
|
|
1274
|
+
content-visibility: auto;
|
|
1275
|
+
contain-intrinsic-size: auto 18rem;
|
|
1276
|
+
}
|
|
938
1277
|
.card {
|
|
939
1278
|
display: flex;
|
|
940
1279
|
flex-direction: column;
|
|
941
|
-
gap: var(--grim-space-
|
|
1280
|
+
gap: var(--grim-space-5);
|
|
942
1281
|
background: var(--grim-color-card);
|
|
943
1282
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
944
|
-
border-radius: var(--grim-radius-
|
|
1283
|
+
border-radius: var(--grim-radius-surface);
|
|
945
1284
|
padding: var(--grim-space-6) var(--grim-space-6);
|
|
1285
|
+
/* Crops the kind watermark to the card's rounded edge, and gives
|
|
1286
|
+
it a stacking context to sit behind in. Without `isolation` a
|
|
1287
|
+
`z-index: -1` child escapes to the root and disappears under the
|
|
1288
|
+
card's own background instead of showing through it. */
|
|
1289
|
+
overflow: hidden;
|
|
1290
|
+
isolation: isolate;
|
|
946
1291
|
/* Whole card navigates to the detail page via the stretched title
|
|
947
1292
|
link below; real controls inside re-stack above the overlay. */
|
|
948
1293
|
position: relative;
|
|
@@ -951,55 +1296,126 @@ function repoLabel(url: string): string {
|
|
|
951
1296
|
}
|
|
952
1297
|
/* Card highlight yields while an inner control is hovered, so its
|
|
953
1298
|
own hover state reads as the click target instead. */
|
|
954
|
-
.card:hover:not(
|
|
955
|
-
:has(.keywords :hover, .copy-group :hover, .source:hover)
|
|
956
|
-
) {
|
|
1299
|
+
.card:hover:not(:has(.keywords :hover, .copy-group :hover, .rating-group a:hover)) {
|
|
957
1300
|
border-color: var(--grim-color-accent);
|
|
958
1301
|
}
|
|
959
|
-
/* Keyboard focus
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1302
|
+
/* Keyboard focus reads as a thicker edge, not a second decoration:
|
|
1303
|
+
the border takes the accent and a 2px outline sits directly
|
|
1304
|
+
outside it, so the two merge into one ring that follows the card's
|
|
1305
|
+
radius. Matched to the list view's focus ring, which is the same
|
|
1306
|
+
2px — a card and a row are the same Tab stop in two shapes, and a
|
|
1307
|
+
focus indicator that changes weight between them reads as two
|
|
1308
|
+
different states. */
|
|
963
1309
|
.card:focus-visible {
|
|
964
|
-
outline:
|
|
1310
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1311
|
+
outline-offset: 0;
|
|
965
1312
|
border-color: var(--grim-color-accent);
|
|
966
1313
|
}
|
|
1314
|
+
/* Makes the whole card the package link. Everything interactive
|
|
1315
|
+
inside it therefore has to be lifted above this — `z-index: 0`
|
|
1316
|
+
here, `1` on each control, so the contract is two stated numbers
|
|
1317
|
+
rather than a tree-order tie that a markup edit can reverse. */
|
|
967
1318
|
.card h2 a::after {
|
|
968
1319
|
content: "";
|
|
969
1320
|
position: absolute;
|
|
970
1321
|
inset: 0;
|
|
1322
|
+
z-index: 0;
|
|
971
1323
|
}
|
|
972
1324
|
.card .keywords,
|
|
973
|
-
.card .copy-group
|
|
974
|
-
.card .source {
|
|
1325
|
+
.card .copy-group {
|
|
975
1326
|
position: relative;
|
|
976
1327
|
}
|
|
1328
|
+
/* Two rows — the name, then the address — with the logo spanning
|
|
1329
|
+
both down the left. A grid rather than the flex row it was: the
|
|
1330
|
+
name has to be able to shrink to nothing so it can ellipsis, and
|
|
1331
|
+
the address has to line up under it rather than under the logo.
|
|
1332
|
+
Nothing shares those rows: every badge that tried cost the name
|
|
1333
|
+
its width on a narrow card. They live in `.card-tags` below. */
|
|
977
1334
|
.card-head {
|
|
978
|
-
display:
|
|
1335
|
+
display: grid;
|
|
1336
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
979
1337
|
align-items: center;
|
|
980
|
-
gap: var(--grim-space-4);
|
|
1338
|
+
column-gap: var(--grim-space-4);
|
|
1339
|
+
}
|
|
1340
|
+
.card-head .card-logo {
|
|
1341
|
+
grid-row: 1 / span 2;
|
|
1342
|
+
}
|
|
1343
|
+
/* The address line runs the full width under the name: the kind is
|
|
1344
|
+
a word on it now, not a badge in the head, so nothing sits to its
|
|
1345
|
+
right to make room for. */
|
|
1346
|
+
.card-head .namespace {
|
|
1347
|
+
grid-column: 2 / -1;
|
|
1348
|
+
grid-row: 2;
|
|
1349
|
+
}
|
|
1350
|
+
.card-head h2 {
|
|
1351
|
+
grid-column: 2;
|
|
1352
|
+
grid-row: 1;
|
|
1353
|
+
}
|
|
1354
|
+
/* Level with the name, in the card's top-right corner. The only
|
|
1355
|
+
thing that shares this row — the kind badge that used to sit here
|
|
1356
|
+
went down to the address line, which is what stopped the name
|
|
1357
|
+
being squeezed between two of them. */
|
|
1358
|
+
.card-head .rating-group {
|
|
1359
|
+
grid-column: 3;
|
|
1360
|
+
grid-row: 1;
|
|
1361
|
+
justify-self: end;
|
|
1362
|
+
}
|
|
1363
|
+
/* Texture, not an icon: oversized and hung off the card's own
|
|
1364
|
+
bottom-right corner, so `overflow: hidden` clips it to the rounded
|
|
1365
|
+
edge and only part of the glyph is on the card at all.
|
|
1366
|
+
The overhang is a share of the glyph, not a length: `translate`
|
|
1367
|
+
resolves its percentages against the element's own box, so the
|
|
1368
|
+
card and the detail panel hang their marks out by the same
|
|
1369
|
+
fraction even though the two are drawn at different sizes. An
|
|
1370
|
+
inset percentage would not — those resolve against the container,
|
|
1371
|
+
which is a different width on each page. */
|
|
1372
|
+
.card-watermark {
|
|
1373
|
+
position: absolute;
|
|
1374
|
+
z-index: -1;
|
|
1375
|
+
/* On the element, never on the stroke colour. The glyph is
|
|
1376
|
+
overlapping stroked paths; a translucent colour composites each
|
|
1377
|
+
crossing twice and leaves the joins visibly darker than the
|
|
1378
|
+
lines they join. Opacity here rasterizes the whole glyph at full
|
|
1379
|
+
strength and blends that once, so the tint is flat. */
|
|
1380
|
+
opacity: 0.14;
|
|
1381
|
+
bottom: 0;
|
|
1382
|
+
right: 0;
|
|
1383
|
+
transform: translate(26%, 26%);
|
|
1384
|
+
pointer-events: none;
|
|
981
1385
|
}
|
|
982
1386
|
.card h2 {
|
|
983
|
-
flex: 1;
|
|
984
1387
|
min-width: 0;
|
|
985
1388
|
margin: 0;
|
|
986
1389
|
font-size: var(--grim-text-lg);
|
|
1390
|
+
/* Tight, so the address sits under the name rather than a line
|
|
1391
|
+
away from it — the two are one label in two parts, and the
|
|
1392
|
+
body's reading leading put a paragraph break between them. */
|
|
1393
|
+
line-height: 1.2;
|
|
987
1394
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
988
|
-
word-break: break-word;
|
|
989
1395
|
}
|
|
1396
|
+
/* One line, always. A package name is a single token, so wrapping it
|
|
1397
|
+
gave a two-line head on a long name and a ragged grid; the ellipsis
|
|
1398
|
+
keeps every card's head the same height, and the full name is on
|
|
1399
|
+
the page this links to. Colour and underline come from the card,
|
|
1400
|
+
not from the link default — the whole card is the link, so the
|
|
1401
|
+
name is a heading that happens to be clickable. */
|
|
990
1402
|
.card h2 a {
|
|
1403
|
+
display: block;
|
|
1404
|
+
overflow: hidden;
|
|
1405
|
+
text-overflow: ellipsis;
|
|
1406
|
+
white-space: nowrap;
|
|
991
1407
|
color: inherit;
|
|
992
1408
|
text-decoration: none;
|
|
993
1409
|
}
|
|
1410
|
+
/* Geometry, not a scale step: the tile is sized to the two rows it
|
|
1411
|
+
spans — the name line plus the address line under it — so it reads
|
|
1412
|
+
as one block beside them rather than a badge floating against the
|
|
1413
|
+
first. `.logo-mark` is a percentage of this, so the placeholder
|
|
1414
|
+
and the broken glyph follow on their own. */
|
|
994
1415
|
.card-logo {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
border-radius: var(--grim-radius-md);
|
|
999
|
-
}
|
|
1000
|
-
img.card-logo {
|
|
1001
|
-
object-fit: contain;
|
|
1002
|
-
background: var(--grim-color-chip-bg);
|
|
1416
|
+
width: 36px;
|
|
1417
|
+
height: 36px;
|
|
1418
|
+
border-radius: var(--grim-radius-inset);
|
|
1003
1419
|
}
|
|
1004
1420
|
.card-logo-fallback {
|
|
1005
1421
|
display: flex;
|
|
@@ -1007,7 +1423,8 @@ function repoLabel(url: string): string {
|
|
|
1007
1423
|
justify-content: center;
|
|
1008
1424
|
color: var(--grim-color-on-accent);
|
|
1009
1425
|
font-weight: 700;
|
|
1010
|
-
|
|
1426
|
+
/* Up a step with the tile: `sm` left the letter adrift in 40px. */
|
|
1427
|
+
font-size: var(--grim-text-lg);
|
|
1011
1428
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1012
1429
|
}
|
|
1013
1430
|
/* THE LOGO SLOT — one box, three states, no layout shift.
|
|
@@ -1020,17 +1437,26 @@ function repoLabel(url: string): string {
|
|
|
1020
1437
|
|
|
1021
1438
|
loading placeholder, no slash — the resting state
|
|
1022
1439
|
ready image faded in, placeholder faded out
|
|
1023
|
-
broken placeholder with its slash
|
|
1440
|
+
broken placeholder with its slash
|
|
1024
1441
|
|
|
1025
1442
|
Kept distinct from `.card-logo-fallback`: that is a package with
|
|
1026
1443
|
no logo at all, which is normal, not a fault. */
|
|
1444
|
+
/* No background. A logo is already a designed mark on its own ground,
|
|
1445
|
+
and a tinted square behind it framed it twice — worst on the many
|
|
1446
|
+
logos that are themselves a rounded square, which then sat inside a
|
|
1447
|
+
slightly larger rounded square. What is left is a box with a size,
|
|
1448
|
+
so the three states still swap without shifting anything. Nothing is
|
|
1449
|
+
drawn in any of them: the placeholder and the broken glyph carry
|
|
1450
|
+
their own colour, and the slash is what separates the two — a
|
|
1451
|
+
dashed frame around the broken one read as a fault in the layout
|
|
1452
|
+
rather than in the image, which is not what it means. The state is
|
|
1453
|
+
still announced, through `role="img"` and a label on the slot. */
|
|
1027
1454
|
.logo-slot {
|
|
1028
1455
|
position: relative;
|
|
1029
1456
|
display: flex;
|
|
1030
1457
|
align-items: center;
|
|
1031
1458
|
justify-content: center;
|
|
1032
1459
|
overflow: hidden;
|
|
1033
|
-
background: var(--grim-color-chip-bg);
|
|
1034
1460
|
color: var(--grim-color-muted);
|
|
1035
1461
|
}
|
|
1036
1462
|
.logo-slot .logo-mark,
|
|
@@ -1063,15 +1489,220 @@ function repoLabel(url: string): string {
|
|
|
1063
1489
|
.logo-slot:not([data-state="broken"]) .mark-broken {
|
|
1064
1490
|
display: none;
|
|
1065
1491
|
}
|
|
1066
|
-
.logo-slot[data-state="broken"] {
|
|
1067
|
-
border: var(--grim-border-width) dashed var(--grim-color-border);
|
|
1068
|
-
}
|
|
1069
1492
|
@media (prefers-reduced-motion: reduce) {
|
|
1070
1493
|
.logo-slot .logo-mark,
|
|
1071
1494
|
.logo-slot img {
|
|
1072
1495
|
transition: none;
|
|
1073
1496
|
}
|
|
1074
1497
|
}
|
|
1498
|
+
/* The list view: a grid of row anchors, not a `<table>`.
|
|
1499
|
+
No headers, deliberately — a header row that cannot sort is a
|
|
1500
|
+
header row that looks like it sorts, and sorting lives in the
|
|
1501
|
+
toolbar. With the headers gone there are no column semantics left
|
|
1502
|
+
to justify the element, and what remains is a list of links.
|
|
1503
|
+
`subgrid` keeps the columns aligned without the markup.
|
|
1504
|
+
|
|
1505
|
+
The container owns the border, the radius and the clip; the rows
|
|
1506
|
+
own the rules between them. That is what makes the corners round
|
|
1507
|
+
in every state — a row's hover fill and focus ring are clipped to
|
|
1508
|
+
the container rather than cutting square across it. */
|
|
1509
|
+
.table {
|
|
1510
|
+
display: grid;
|
|
1511
|
+
/* logo | kind | name | description | updated. The two fixed-width
|
|
1512
|
+
marks lead, so the names start on one line down the whole list
|
|
1513
|
+
and the eye runs the column it actually reads. The name is
|
|
1514
|
+
capped rather than `max-content`: an unbounded name ate the
|
|
1515
|
+
description's space, and past the cap both clip instead. The
|
|
1516
|
+
description is the only track that grows. */
|
|
1517
|
+
grid-template-columns: auto auto minmax(8rem, 14rem) 1fr auto;
|
|
1518
|
+
background: var(--grim-color-card);
|
|
1519
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1520
|
+
/* `xl`, the content-surface step — the same one the card wears.
|
|
1521
|
+
The two are one list in two shapes, so a reader switching between
|
|
1522
|
+
them should see the corner stay put. It was `lg`, the control
|
|
1523
|
+
step, which put the table's corners with the sort pair's rather
|
|
1524
|
+
than with the cards'. */
|
|
1525
|
+
border-radius: var(--grim-radius-surface);
|
|
1526
|
+
overflow: hidden;
|
|
1527
|
+
}
|
|
1528
|
+
/* One more track when the index publishes ratings. Decided once per
|
|
1529
|
+
index, never per filter — see `PackageTable`. */
|
|
1530
|
+
.table.rated {
|
|
1531
|
+
grid-template-columns: auto auto minmax(8rem, 14rem) 1fr auto auto;
|
|
1532
|
+
}
|
|
1533
|
+
.table .row {
|
|
1534
|
+
grid-column: 1 / -1;
|
|
1535
|
+
display: grid;
|
|
1536
|
+
grid-template-columns: subgrid;
|
|
1537
|
+
align-items: center;
|
|
1538
|
+
gap: var(--grim-space-5);
|
|
1539
|
+
padding: var(--grim-space-3) var(--grim-space-5);
|
|
1540
|
+
color: inherit;
|
|
1541
|
+
text-decoration: none;
|
|
1542
|
+
font-size: var(--grim-text-sm);
|
|
1543
|
+
transition: background-color var(--grim-duration-base) ease;
|
|
1544
|
+
}
|
|
1545
|
+
/* The rule belongs between rows, not under the last one — which the
|
|
1546
|
+
container's own border already draws. */
|
|
1547
|
+
.table .row + .row {
|
|
1548
|
+
border-top: var(--grim-border-width) solid var(--grim-color-border);
|
|
1549
|
+
}
|
|
1550
|
+
.table .row:hover {
|
|
1551
|
+
background: var(--grim-color-chip-bg);
|
|
1552
|
+
}
|
|
1553
|
+
/* Inset, so the ring stays inside the row: an outline sits outside
|
|
1554
|
+
the border box and the container's `overflow: hidden` would clip
|
|
1555
|
+
it to a stray line on the first and last rows. Pulled in by its
|
|
1556
|
+
own width, which is the a11y floor rather than a style. */
|
|
1557
|
+
.table .row:focus-visible {
|
|
1558
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1559
|
+
outline-offset: -2px;
|
|
1560
|
+
background: var(--grim-color-accent-soft);
|
|
1561
|
+
}
|
|
1562
|
+
/* The two rows that meet the container's corners carry its radius,
|
|
1563
|
+
so a hover fill or a focus ring on either follows the rounding
|
|
1564
|
+
instead of cutting straight across it. */
|
|
1565
|
+
.table .row:first-child {
|
|
1566
|
+
border-start-start-radius: var(--grim-radius-surface);
|
|
1567
|
+
border-start-end-radius: var(--grim-radius-surface);
|
|
1568
|
+
}
|
|
1569
|
+
.table .row:last-child {
|
|
1570
|
+
border-end-start-radius: var(--grim-radius-surface);
|
|
1571
|
+
border-end-end-radius: var(--grim-radius-surface);
|
|
1572
|
+
}
|
|
1573
|
+
/* Exactly one line per cell, always. Even rows are the whole reason
|
|
1574
|
+
to prefer this view to the cards: a cell that wraps makes its row
|
|
1575
|
+
taller than its neighbours, and a column the eye can no longer run
|
|
1576
|
+
down is a list doing the cards' job badly. */
|
|
1577
|
+
.table .row > * {
|
|
1578
|
+
min-width: 0;
|
|
1579
|
+
white-space: nowrap;
|
|
1580
|
+
overflow: hidden;
|
|
1581
|
+
text-overflow: ellipsis;
|
|
1582
|
+
/* Every cell is one text line and no cell is a flex box, so all
|
|
1583
|
+
five have the same height and the same baseline — the icons ride
|
|
1584
|
+
that line on `vertical-align` rather than being centred as boxes
|
|
1585
|
+
of their own. Cell-by-cell centring is what left the marks and
|
|
1586
|
+
the words a pixel apart; this makes them the same line. It is
|
|
1587
|
+
also what keeps `text-overflow` working, which a flex container
|
|
1588
|
+
would have silently dropped. */
|
|
1589
|
+
line-height: 1.6;
|
|
1590
|
+
}
|
|
1591
|
+
/* One glyph wide, and the same width whatever the kind is — so every
|
|
1592
|
+
name beside it starts on one line down the whole list. The colour
|
|
1593
|
+
is set inline, from the kind's own token or the deprecation one. */
|
|
1594
|
+
.t-kind {
|
|
1595
|
+
justify-content: center;
|
|
1596
|
+
}
|
|
1597
|
+
/* The card's tile at a row's height. Geometry, not a scale step, and
|
|
1598
|
+
measured against the text line beside it rather than against the
|
|
1599
|
+
kind mark: 20px is the tile that reads as belonging to the name
|
|
1600
|
+
without making the row taller than one line of type. `.logo-mark`
|
|
1601
|
+
and the fallback letter are proportions of the tile, so both follow
|
|
1602
|
+
on their own — only the letter needs a step down, since the card's
|
|
1603
|
+
is sized for a 36px tile. */
|
|
1604
|
+
.t-logo .card-logo {
|
|
1605
|
+
width: 20px;
|
|
1606
|
+
height: 20px;
|
|
1607
|
+
}
|
|
1608
|
+
.t-logo .card-logo-fallback {
|
|
1609
|
+
font-size: var(--grim-text-2xs);
|
|
1610
|
+
}
|
|
1611
|
+
/* The fallback for a kind this renderer has no mark for. */
|
|
1612
|
+
.t-kind-word {
|
|
1613
|
+
font-size: var(--grim-text-2xs);
|
|
1614
|
+
color: var(--grim-color-muted);
|
|
1615
|
+
}
|
|
1616
|
+
.t-name {
|
|
1617
|
+
color: var(--grim-color-fg);
|
|
1618
|
+
font-weight: 600;
|
|
1619
|
+
}
|
|
1620
|
+
.t-desc,
|
|
1621
|
+
.t-updated {
|
|
1622
|
+
color: var(--grim-color-muted);
|
|
1623
|
+
}
|
|
1624
|
+
/* Last column, held at the right edge: the count, then its arrow.
|
|
1625
|
+
The two sit next to each other rather than at opposite sides of a
|
|
1626
|
+
wide cell — they are one reading, and the gap that separated them
|
|
1627
|
+
was the cell's width showing through. */
|
|
1628
|
+
.t-rating {
|
|
1629
|
+
justify-content: flex-end;
|
|
1630
|
+
color: var(--grim-color-muted);
|
|
1631
|
+
}
|
|
1632
|
+
.t-rating svg {
|
|
1633
|
+
margin-left: var(--grim-space-2);
|
|
1634
|
+
}
|
|
1635
|
+
/* The two cells that carry a glyph centre it as a box, not on the
|
|
1636
|
+
text line. `vertical-align: middle` aligns to half the x-height,
|
|
1637
|
+
which sits about 2px under the line's true middle at this step —
|
|
1638
|
+
the eye reads that as the mark and the arrow sagging below the
|
|
1639
|
+
words beside them. Neither cell owes anything to the row's
|
|
1640
|
+
no-flex rule above: the kind is one mark and the rating is a
|
|
1641
|
+
fixed-width count plus its arrow, so there is no text here for
|
|
1642
|
+
`text-overflow` to elide. */
|
|
1643
|
+
.t-kind,
|
|
1644
|
+
.t-rating {
|
|
1645
|
+
display: flex;
|
|
1646
|
+
align-items: center;
|
|
1647
|
+
}
|
|
1648
|
+
/* The count carries the column's alignment, and the arrow after it
|
|
1649
|
+
inherits it: a fixed box, right-aligned, in tabular figures. Without
|
|
1650
|
+
the fixed box a three-digit count would push its own arrow right of
|
|
1651
|
+
the one above it. Geometry — it holds four digits at this step. */
|
|
1652
|
+
.t-votes {
|
|
1653
|
+
/* Never shrunk to its digits: the box is what holds the arrow
|
|
1654
|
+
after it in one place down the column. */
|
|
1655
|
+
flex: none;
|
|
1656
|
+
width: 2rem;
|
|
1657
|
+
text-align: right;
|
|
1658
|
+
font-variant-numeric: tabular-nums;
|
|
1659
|
+
}
|
|
1660
|
+
/* Every cell names its own column rather than being auto-placed.
|
|
1661
|
+
Auto-placement fills tracks in document order, so the moment a cell
|
|
1662
|
+
is dropped — which the narrow layout below does to the description
|
|
1663
|
+
— every cell after it slides one track left and the columns stop
|
|
1664
|
+
meaning what they did. Naming them makes dropping one safe. */
|
|
1665
|
+
.t-logo {
|
|
1666
|
+
grid-column: 1;
|
|
1667
|
+
}
|
|
1668
|
+
.t-kind {
|
|
1669
|
+
grid-column: 2;
|
|
1670
|
+
}
|
|
1671
|
+
.t-name {
|
|
1672
|
+
grid-column: 3;
|
|
1673
|
+
}
|
|
1674
|
+
.t-desc {
|
|
1675
|
+
grid-column: 4;
|
|
1676
|
+
}
|
|
1677
|
+
.t-updated {
|
|
1678
|
+
grid-column: 5;
|
|
1679
|
+
}
|
|
1680
|
+
.t-rating {
|
|
1681
|
+
grid-column: 6;
|
|
1682
|
+
}
|
|
1683
|
+
/* Narrow viewports lose the description rather than scroll sideways:
|
|
1684
|
+
it is context for the name beside it, and the detail page states it
|
|
1685
|
+
in full. The remaining cells move up a track with it, so no empty
|
|
1686
|
+
column and no stray gap is left behind. */
|
|
1687
|
+
@media (max-width: 40rem) {
|
|
1688
|
+
.table,
|
|
1689
|
+
.table.rated {
|
|
1690
|
+
grid-template-columns: auto auto minmax(6rem, 1fr) auto;
|
|
1691
|
+
}
|
|
1692
|
+
.table.rated {
|
|
1693
|
+
grid-template-columns: auto auto minmax(6rem, 1fr) auto auto;
|
|
1694
|
+
}
|
|
1695
|
+
.t-desc {
|
|
1696
|
+
display: none;
|
|
1697
|
+
}
|
|
1698
|
+
.t-updated {
|
|
1699
|
+
grid-column: 4;
|
|
1700
|
+
}
|
|
1701
|
+
.t-rating {
|
|
1702
|
+
grid-column: 5;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1075
1706
|
.badge {
|
|
1076
1707
|
flex-shrink: 0;
|
|
1077
1708
|
font-size: var(--grim-text-2xs);
|
|
@@ -1106,78 +1737,180 @@ function repoLabel(url: string): string {
|
|
|
1106
1737
|
.chip[class*="kind-"] {
|
|
1107
1738
|
color: var(--grim-color-fg);
|
|
1108
1739
|
}
|
|
1740
|
+
/* A step below the name and quieter: where the package lives is
|
|
1741
|
+
context for the name above it, not a second heading. */
|
|
1109
1742
|
.namespace {
|
|
1110
1743
|
margin: 0;
|
|
1111
1744
|
color: var(--grim-color-muted);
|
|
1112
|
-
font-size: var(--grim-text-
|
|
1745
|
+
font-size: var(--grim-text-xs);
|
|
1746
|
+
line-height: 1.3;
|
|
1113
1747
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1114
1748
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1749
|
+
/* The rating is one control with two halves — the count, which opens
|
|
1750
|
+
the forge thread, and the vote, which casts one — joined the way
|
|
1751
|
+
the sort combo is. It was two badges with a dash between them,
|
|
1752
|
+
which read as punctuation rather than as a control. */
|
|
1753
|
+
.rating-group {
|
|
1754
|
+
display: inline-flex;
|
|
1755
|
+
align-items: stretch;
|
|
1756
|
+
/* Above the card's full-card overlay (`.card h2 a::after`), so each
|
|
1757
|
+
half takes its own click instead of opening the package page.
|
|
1758
|
+
Stated rather than left to tree order: the overlay and these are
|
|
1759
|
+
both positioned, and "later in the DOM wins" is the kind of tie
|
|
1760
|
+
a later markup edit breaks silently. */
|
|
1761
|
+
position: relative;
|
|
1762
|
+
z-index: 1;
|
|
1120
1763
|
}
|
|
1121
|
-
|
|
1764
|
+
/* Same treatment `.pill` gives the version and licence badges beside
|
|
1765
|
+
them, applied per half so the border can be shared. */
|
|
1766
|
+
.rating-count,
|
|
1767
|
+
.rating-vote {
|
|
1768
|
+
display: inline-flex;
|
|
1769
|
+
align-items: center;
|
|
1770
|
+
gap: var(--grim-space-1);
|
|
1122
1771
|
font-size: var(--grim-text-xs);
|
|
1123
1772
|
color: var(--grim-color-muted);
|
|
1124
1773
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1125
|
-
|
|
1126
|
-
/* Optical, as above — the smallest badge in the UI. */
|
|
1774
|
+
/* Optical, matching `.pill` — the smallest badge in the UI. */
|
|
1127
1775
|
padding: 0.03rem var(--grim-space-4);
|
|
1776
|
+
text-decoration: none;
|
|
1128
1777
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1778
|
+
/* Lifted individually as well as through the group: whichever of the
|
|
1779
|
+
two ends up establishing a stacking context, the link is above the
|
|
1780
|
+
card overlay and takes its own click. */
|
|
1781
|
+
.rating-count,
|
|
1782
|
+
.rating-vote {
|
|
1783
|
+
position: relative;
|
|
1784
|
+
z-index: 1;
|
|
1785
|
+
}
|
|
1786
|
+
.rating-count {
|
|
1787
|
+
border-radius: var(--grim-radius-pill) 0 0 var(--grim-radius-pill);
|
|
1788
|
+
/* Opaque, like its tinted other half. A bordered control with a
|
|
1789
|
+
see-through interior let the kind watermark run straight through
|
|
1790
|
+
the count — the one place on the card where a big glyph and a
|
|
1791
|
+
small control land on the same corner. */
|
|
1792
|
+
background: var(--grim-color-card);
|
|
1131
1793
|
}
|
|
1132
|
-
/*
|
|
1133
|
-
|
|
1134
|
-
.
|
|
1135
|
-
|
|
1136
|
-
align-items: center;
|
|
1137
|
-
gap: var(--grim-space-1);
|
|
1794
|
+
/* An index with no VS Code extension configured shows the count
|
|
1795
|
+
alone, and a lone half is a whole pill again. */
|
|
1796
|
+
.rating-count:last-child {
|
|
1797
|
+
border-radius: var(--grim-radius-pill);
|
|
1138
1798
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1799
|
+
/* Tinted rather than outlined differently: this is the half that
|
|
1800
|
+
posts something, and a shade says so without a second border
|
|
1801
|
+
colour competing with the hover state. Pulled onto its neighbour's
|
|
1802
|
+
border so the seam is one line, derived from the token. */
|
|
1803
|
+
.rating-vote {
|
|
1804
|
+
border-radius: 0 var(--grim-radius-pill) var(--grim-radius-pill) 0;
|
|
1805
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
1806
|
+
background: var(--grim-color-chip-bg);
|
|
1807
|
+
/* Wider on the outside than the seam: the glyph sat against the
|
|
1808
|
+
rounded edge, which reads as clipped rather than as inset. */
|
|
1809
|
+
padding-inline: var(--grim-space-3) var(--grim-space-5);
|
|
1142
1810
|
}
|
|
1143
|
-
.
|
|
1811
|
+
a.rating-count:hover,
|
|
1812
|
+
a.rating-vote:hover {
|
|
1813
|
+
color: var(--grim-color-accent);
|
|
1814
|
+
border-color: var(--grim-color-accent);
|
|
1815
|
+
/* 2, not 1: both halves already sit at 1 so they clear the card
|
|
1816
|
+
overlay, and the vote half is pulled onto the count's right
|
|
1817
|
+
border by a negative margin. At equal z-index the later element
|
|
1818
|
+
wins, so hovering the count lit an edge its neighbour was
|
|
1819
|
+
covering — the accent appeared on three sides and stopped. */
|
|
1820
|
+
z-index: 2;
|
|
1821
|
+
}
|
|
1822
|
+
/* Version and recency as one aside. Italic and a step below the rest
|
|
1823
|
+
on purpose — it is a note about the card, not one of its facts, and
|
|
1824
|
+
it sits opposite two buttons that should keep the eye. Bottom-
|
|
1825
|
+
aligned rather than centred on them, since a centred aside reads as
|
|
1826
|
+
a third control in the row. */
|
|
1827
|
+
.card-meta {
|
|
1144
1828
|
margin: 0;
|
|
1145
|
-
|
|
1146
|
-
|
|
1829
|
+
align-self: flex-end;
|
|
1830
|
+
font-size: var(--grim-text-2xs);
|
|
1831
|
+
font-style: italic;
|
|
1832
|
+
color: var(--grim-color-muted);
|
|
1833
|
+
white-space: nowrap;
|
|
1834
|
+
}
|
|
1835
|
+
.card-version {
|
|
1836
|
+
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1837
|
+
font-style: normal;
|
|
1147
1838
|
}
|
|
1148
1839
|
/* Long descriptions would stretch their whole grid row; clamp to 3
|
|
1149
1840
|
lines so cards stay uniform. Full text lives on the detail page. */
|
|
1841
|
+
/* Exactly two lines, whether it needs them or not.
|
|
1842
|
+
`line-clamp` alone only caps the tall ones; a one-line description
|
|
1843
|
+
still drew a shorter block, so every card in a row started its
|
|
1844
|
+
keywords and its buttons at a different height. `min-height` is the
|
|
1845
|
+
other half — two lines of this leading, reserved — and the two
|
|
1846
|
+
together make the block a fixed size regardless of the text. */
|
|
1150
1847
|
.description {
|
|
1151
1848
|
margin: 0;
|
|
1152
1849
|
font-size: var(--grim-text-md);
|
|
1850
|
+
line-height: 1.45;
|
|
1851
|
+
min-height: calc(2 * 1.45em);
|
|
1153
1852
|
/* `auto` basis, not `0`: overflow:hidden drops the min-height:auto
|
|
1154
1853
|
floor, so a 0-basis item can collapse in an auto-height column. */
|
|
1155
|
-
flex:
|
|
1854
|
+
flex: 0 0 auto;
|
|
1156
1855
|
display: -webkit-box;
|
|
1157
1856
|
-webkit-box-orient: vertical;
|
|
1158
|
-
-webkit-line-clamp:
|
|
1159
|
-
line-clamp:
|
|
1857
|
+
-webkit-line-clamp: 2;
|
|
1858
|
+
line-clamp: 2;
|
|
1160
1859
|
overflow: hidden;
|
|
1161
1860
|
}
|
|
1861
|
+
/* One row, always. A card in an auto-fill grid has to keep its
|
|
1862
|
+
height predictable, and a package with long keywords was wrapping
|
|
1863
|
+
to a second line and pushing the foot down out of step with its
|
|
1864
|
+
neighbours.
|
|
1865
|
+
|
|
1866
|
+
Clipped rather than counted: a "+N" chip sits at the end of the
|
|
1867
|
+
row, so it is the first thing the clip eats, and it would be
|
|
1868
|
+
claiming a number while the row silently hid more. The fade says
|
|
1869
|
+
"there is more" without naming a figure, and the full list is on
|
|
1870
|
+
the page the card links to. Same treatment the command field uses
|
|
1871
|
+
for its overflow. */
|
|
1872
|
+
/* One row, whatever it holds: `nowrap` plus `hidden` means a set of
|
|
1873
|
+
keywords too wide for the card is cut off rather than wrapped onto
|
|
1874
|
+
a second row that would unbalance the grid.
|
|
1875
|
+
|
|
1876
|
+
The fade is on the row, which is full-width — so it always sits at
|
|
1877
|
+
the card's right edge, and on a row whose chips fit it falls
|
|
1878
|
+
entirely on empty space and shows nothing. Overflow is exactly the
|
|
1879
|
+
case where a chip reaches under it, which is why no measurement
|
|
1880
|
+
and no JavaScript is needed to decide. */
|
|
1162
1881
|
.keywords {
|
|
1163
1882
|
display: flex;
|
|
1164
|
-
flex-wrap:
|
|
1883
|
+
flex-wrap: nowrap;
|
|
1165
1884
|
gap: var(--grim-space-2);
|
|
1885
|
+
overflow: hidden;
|
|
1886
|
+
mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
|
|
1887
|
+
}
|
|
1888
|
+
/* Each chip keeps its own width; the row gives up, not the words. */
|
|
1889
|
+
.chip.keyword {
|
|
1890
|
+
flex: 0 0 auto;
|
|
1166
1891
|
}
|
|
1167
1892
|
.chip.keyword {
|
|
1168
1893
|
font-size: var(--grim-text-2xs);
|
|
1169
1894
|
padding: var(--grim-space-1) var(--grim-space-4);
|
|
1170
1895
|
color: var(--grim-color-muted);
|
|
1171
1896
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1897
|
+
/* A card chip applies the keyword facet, so it can be the one already
|
|
1898
|
+
applied — it says so rather than staying muted like the rest. */
|
|
1899
|
+
.chip.keyword.active {
|
|
1900
|
+
color: var(--grim-color-fg);
|
|
1174
1901
|
}
|
|
1175
1902
|
.card-foot {
|
|
1176
1903
|
display: flex;
|
|
1177
1904
|
align-items: center;
|
|
1178
1905
|
justify-content: space-between;
|
|
1179
1906
|
gap: var(--grim-space-4);
|
|
1180
|
-
|
|
1907
|
+
}
|
|
1908
|
+
/* The slack goes above the buttons, so they line up across a row
|
|
1909
|
+
however tall the cards are. The foot is the last group on every
|
|
1910
|
+
card — the keywords moved up under the head — so there is one
|
|
1911
|
+
element to hang it on rather than one per layout. */
|
|
1912
|
+
.card-foot {
|
|
1913
|
+
margin-top: auto;
|
|
1181
1914
|
}
|
|
1182
1915
|
.copy-group {
|
|
1183
1916
|
display: flex;
|
|
@@ -1192,7 +1925,7 @@ function repoLabel(url: string): string {
|
|
|
1192
1925
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1193
1926
|
background: var(--grim-color-chip-bg);
|
|
1194
1927
|
color: var(--grim-color-fg);
|
|
1195
|
-
border-radius: var(--grim-radius-
|
|
1928
|
+
border-radius: var(--grim-radius-inset);
|
|
1196
1929
|
padding: var(--grim-space-2) var(--grim-space-4);
|
|
1197
1930
|
font-size: var(--grim-text-sm);
|
|
1198
1931
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
@@ -1228,7 +1961,7 @@ function repoLabel(url: string): string {
|
|
|
1228
1961
|
margin: 0;
|
|
1229
1962
|
padding: var(--grim-space-6) var(--grim-space-6);
|
|
1230
1963
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1231
|
-
border-radius: var(--grim-radius-
|
|
1964
|
+
border-radius: var(--grim-radius-control);
|
|
1232
1965
|
overflow-x: auto;
|
|
1233
1966
|
font-size: var(--grim-text-sm);
|
|
1234
1967
|
line-height: 1.5;
|
|
@@ -1254,12 +1987,21 @@ function repoLabel(url: string): string {
|
|
|
1254
1987
|
.astro-code span:not([style*="background"]) {
|
|
1255
1988
|
background-color: transparent !important;
|
|
1256
1989
|
}
|
|
1257
|
-
/*
|
|
1258
|
-
|
|
1259
|
-
|
|
1990
|
+
/* The corner the block's buttons sit in. It is a row rather than one
|
|
1991
|
+
positioned button because `CodeBlock.astro` can add a second one —
|
|
1992
|
+
a VS Code deep link — and the copy button `Base.astro` injects has
|
|
1993
|
+
to land beside it without either knowing the other's width. */
|
|
1994
|
+
.code-actions {
|
|
1260
1995
|
position: absolute;
|
|
1261
1996
|
top: 0.45rem;
|
|
1262
1997
|
right: 0.45rem;
|
|
1998
|
+
display: flex;
|
|
1999
|
+
gap: var(--grim-space-2);
|
|
2000
|
+
}
|
|
2001
|
+
/* Reveals on hover or keyboard focus, so they are not a permanent
|
|
2002
|
+
smudge over the first line of every block. */
|
|
2003
|
+
.code-copy,
|
|
2004
|
+
.code-vscode {
|
|
1263
2005
|
display: inline-flex;
|
|
1264
2006
|
align-items: center;
|
|
1265
2007
|
justify-content: center;
|
|
@@ -1267,9 +2009,10 @@ function repoLabel(url: string): string {
|
|
|
1267
2009
|
height: 1.9rem;
|
|
1268
2010
|
padding: 0;
|
|
1269
2011
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1270
|
-
border-radius: var(--grim-radius-
|
|
2012
|
+
border-radius: var(--grim-radius-inset);
|
|
1271
2013
|
background: var(--grim-color-card);
|
|
1272
2014
|
color: var(--grim-color-muted);
|
|
2015
|
+
text-decoration: none;
|
|
1273
2016
|
cursor: pointer;
|
|
1274
2017
|
opacity: 0;
|
|
1275
2018
|
transition:
|
|
@@ -1278,10 +2021,13 @@ function repoLabel(url: string): string {
|
|
|
1278
2021
|
border-color var(--grim-duration-fast) ease;
|
|
1279
2022
|
}
|
|
1280
2023
|
.code-block:hover .code-copy,
|
|
1281
|
-
.code-
|
|
2024
|
+
.code-block:hover .code-vscode,
|
|
2025
|
+
.code-copy:focus-visible,
|
|
2026
|
+
.code-vscode:focus-visible {
|
|
1282
2027
|
opacity: 1;
|
|
1283
2028
|
}
|
|
1284
|
-
.code-copy:hover
|
|
2029
|
+
.code-copy:hover,
|
|
2030
|
+
.code-vscode:hover {
|
|
1285
2031
|
color: var(--grim-color-accent);
|
|
1286
2032
|
border-color: var(--grim-color-accent);
|
|
1287
2033
|
}
|
|
@@ -1291,7 +2037,8 @@ function repoLabel(url: string): string {
|
|
|
1291
2037
|
opacity: 1;
|
|
1292
2038
|
}
|
|
1293
2039
|
@media (prefers-reduced-motion: reduce) {
|
|
1294
|
-
.code-copy
|
|
2040
|
+
.code-copy,
|
|
2041
|
+
.code-vscode {
|
|
1295
2042
|
transition: none;
|
|
1296
2043
|
}
|
|
1297
2044
|
}
|
|
@@ -1305,10 +2052,6 @@ function repoLabel(url: string): string {
|
|
|
1305
2052
|
gap: 0;
|
|
1306
2053
|
text-decoration: none;
|
|
1307
2054
|
}
|
|
1308
|
-
.source {
|
|
1309
|
-
font-size: var(--grim-text-sm);
|
|
1310
|
-
white-space: nowrap;
|
|
1311
|
-
}
|
|
1312
2055
|
.empty {
|
|
1313
2056
|
color: var(--grim-color-muted);
|
|
1314
2057
|
}
|