@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
|
@@ -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
|
|
@@ -404,6 +360,20 @@ function repoLabel(url: string): string {
|
|
|
404
360
|
padding-block: var(--grim-space-5);
|
|
405
361
|
gap: var(--grim-space-6);
|
|
406
362
|
}
|
|
363
|
+
/* Deliberately neutral, not the amber of `--grim-color-banner-*`:
|
|
364
|
+
that palette says "warning", and this line is whatever the index
|
|
365
|
+
has to state on every page. An index that wants it loud restyles
|
|
366
|
+
`[data-slot="site-notice"]` — one selector, no new token. */
|
|
367
|
+
.notice {
|
|
368
|
+
border-bottom: var(--grim-border-width) solid var(--grim-color-border);
|
|
369
|
+
background: var(--grim-color-chip-bg);
|
|
370
|
+
color: var(--grim-color-muted);
|
|
371
|
+
font-size: var(--grim-text-sm);
|
|
372
|
+
}
|
|
373
|
+
.notice p {
|
|
374
|
+
margin: 0;
|
|
375
|
+
padding-block: var(--grim-space-4);
|
|
376
|
+
}
|
|
407
377
|
.brand {
|
|
408
378
|
display: inline-flex;
|
|
409
379
|
align-items: center;
|
|
@@ -506,7 +476,7 @@ function repoLabel(url: string): string {
|
|
|
506
476
|
}
|
|
507
477
|
.hero code {
|
|
508
478
|
background: var(--grim-color-chip-bg);
|
|
509
|
-
border-radius: var(--grim-radius-
|
|
479
|
+
border-radius: var(--grim-radius-code);
|
|
510
480
|
padding: var(--grim-space-1) var(--grim-space-3);
|
|
511
481
|
/* `em`, not a type token: inline code sizes against the prose it
|
|
512
482
|
sits in, so it must follow whatever that text is set at. */
|
|
@@ -521,7 +491,7 @@ function repoLabel(url: string): string {
|
|
|
521
491
|
min-width: 0;
|
|
522
492
|
background: var(--grim-color-card);
|
|
523
493
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
524
|
-
border-radius: var(--grim-radius-
|
|
494
|
+
border-radius: var(--grim-radius-control);
|
|
525
495
|
}
|
|
526
496
|
/* No `overflow: hidden` — that would clip the platform menu. The end
|
|
527
497
|
segments round themselves instead (8px border box, 1px border).
|
|
@@ -677,7 +647,7 @@ function repoLabel(url: string): string {
|
|
|
677
647
|
list-style: none;
|
|
678
648
|
background: var(--grim-color-card);
|
|
679
649
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
680
|
-
border-radius: var(--grim-radius-
|
|
650
|
+
border-radius: var(--grim-radius-control);
|
|
681
651
|
box-shadow: var(--grim-shadow-raised);
|
|
682
652
|
}
|
|
683
653
|
.os-menu li button {
|
|
@@ -686,7 +656,7 @@ function repoLabel(url: string): string {
|
|
|
686
656
|
gap: var(--grim-space-4);
|
|
687
657
|
width: 100%;
|
|
688
658
|
border: none;
|
|
689
|
-
border-radius: var(--grim-radius-
|
|
659
|
+
border-radius: var(--grim-radius-inset);
|
|
690
660
|
background: none;
|
|
691
661
|
padding: var(--grim-space-3) var(--grim-space-4);
|
|
692
662
|
color: var(--grim-color-fg);
|
|
@@ -718,14 +688,19 @@ function repoLabel(url: string): string {
|
|
|
718
688
|
shows until a hover, focus or open state paints a background — then
|
|
719
689
|
square corners poke out past the bar's own 8px rounding.
|
|
720
690
|
The summary is named explicitly rather than left to inherit. */
|
|
691
|
+
/* Clamped at zero, because the radius is a knob now: with
|
|
692
|
+
`--grim-radius-base: 0px` the bare subtraction is `-1px`, and a
|
|
693
|
+
negative `border-radius` is not invalid-but-ignored per corner —
|
|
694
|
+
it drops the whole declaration. Harmless while everything is
|
|
695
|
+
square, and a trap the moment someone rounds the site back up. */
|
|
721
696
|
.cmd-bar > :first-child,
|
|
722
697
|
.cmd-bar > :first-child > summary {
|
|
723
|
-
border-start-start-radius: calc(var(--grim-radius-
|
|
724
|
-
border-end-start-radius: calc(var(--grim-radius-
|
|
698
|
+
border-start-start-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
699
|
+
border-end-start-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
725
700
|
}
|
|
726
701
|
.cmd-bar > :last-child {
|
|
727
|
-
border-start-end-radius: calc(var(--grim-radius-
|
|
728
|
-
border-end-end-radius: calc(var(--grim-radius-
|
|
702
|
+
border-start-end-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
703
|
+
border-end-end-radius: max(0px, calc(var(--grim-radius-control) - var(--grim-border-width)));
|
|
729
704
|
}
|
|
730
705
|
|
|
731
706
|
/* Attribution sits opposite the raw-data line, on the same row where
|
|
@@ -770,7 +745,7 @@ function repoLabel(url: string): string {
|
|
|
770
745
|
padding: var(--grim-space-4) var(--grim-space-5);
|
|
771
746
|
background: var(--grim-color-card);
|
|
772
747
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
773
|
-
border-radius: var(--grim-radius-
|
|
748
|
+
border-radius: var(--grim-radius-surface);
|
|
774
749
|
box-shadow: var(--grim-shadow-overlay);
|
|
775
750
|
/* Out of the layout and unhittable until shown, so it never eats a
|
|
776
751
|
click aimed at whatever is beneath it. */
|
|
@@ -817,22 +792,30 @@ function repoLabel(url: string): string {
|
|
|
817
792
|
visibility: hidden;
|
|
818
793
|
}
|
|
819
794
|
|
|
795
|
+
/* Three stacked rows, not one wrapping line: the search field, then
|
|
796
|
+
the filters, then what they left plus the controls that arrange it.
|
|
797
|
+
A wrapping row put a different set on each line at each viewport
|
|
798
|
+
width, so the toolbar read as a different object on every screen. */
|
|
820
799
|
.controls {
|
|
821
800
|
display: flex;
|
|
822
|
-
flex-
|
|
823
|
-
align-items:
|
|
801
|
+
flex-direction: column;
|
|
802
|
+
align-items: stretch;
|
|
824
803
|
gap: var(--grim-space-5);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
804
|
+
/* Close, because the last row of the toolbar belongs to the list
|
|
805
|
+
below rather than to the toolbar above it — it says what the
|
|
806
|
+
filters left and how it is arranged. Proximity is what groups
|
|
807
|
+
them; `.meta-row` opens the gap on its other side to match. */
|
|
808
|
+
margin-bottom: var(--grim-space-3);
|
|
809
|
+
}
|
|
810
|
+
/* Wide basis, not full width: the field and the sort control share
|
|
811
|
+
the first line and the kind chips wrap to the next, and on a narrow
|
|
812
|
+
viewport the sort control drops below the field instead of
|
|
813
|
+
squeezing it. The container gap separates the rows and reads wider
|
|
814
|
+
than the gap inside a chip group, so the groups still read as
|
|
815
|
+
groups without a wrapper element. */
|
|
832
816
|
.search-field {
|
|
833
817
|
position: relative;
|
|
834
818
|
display: flex;
|
|
835
|
-
flex: 1 1 100%;
|
|
836
819
|
/* Breathing room above the field when `/` scrolls it to the top of
|
|
837
820
|
the viewport, so it does not sit flush against the edge. */
|
|
838
821
|
scroll-margin-top: 1.25rem;
|
|
@@ -844,7 +827,7 @@ function repoLabel(url: string): string {
|
|
|
844
827
|
/* 2.4rem is the hint's measured width, not a rhythm step. */
|
|
845
828
|
padding-right: 2.4rem;
|
|
846
829
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
847
|
-
border-radius: var(--grim-radius-
|
|
830
|
+
border-radius: var(--grim-radius-control);
|
|
848
831
|
background: var(--grim-color-card);
|
|
849
832
|
color: var(--grim-color-fg);
|
|
850
833
|
font-size: var(--grim-text-md);
|
|
@@ -866,7 +849,7 @@ function repoLabel(url: string): string {
|
|
|
866
849
|
pointer-events: none;
|
|
867
850
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
868
851
|
border-bottom-width: 2px;
|
|
869
|
-
border-radius: var(--grim-radius-
|
|
852
|
+
border-radius: var(--grim-radius-inset);
|
|
870
853
|
background: var(--grim-color-chip-bg);
|
|
871
854
|
color: var(--grim-color-muted);
|
|
872
855
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
@@ -878,24 +861,85 @@ function repoLabel(url: string): string {
|
|
|
878
861
|
.search-field:has(input:not(:placeholder-shown)) .search-hint {
|
|
879
862
|
display: none;
|
|
880
863
|
}
|
|
864
|
+
/* One line, always. Wrapping was the defect: at a narrow width the
|
|
865
|
+
row dropped `deprecated` onto a second line, because no chip can
|
|
866
|
+
shrink below its own longest word and the rail's chip count is a
|
|
867
|
+
constant rather than a viewport reading. So the KEYWORD group is
|
|
868
|
+
the single flexible child and scrolls; the kinds and the toggle
|
|
869
|
+
keep their size and their place. */
|
|
870
|
+
.filter-row {
|
|
871
|
+
display: flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: var(--grim-space-3);
|
|
874
|
+
flex-wrap: nowrap;
|
|
875
|
+
/* Backstop for the extreme case where the kinds alone overflow. */
|
|
876
|
+
overflow-x: auto;
|
|
877
|
+
}
|
|
881
878
|
.chips {
|
|
882
879
|
display: flex;
|
|
883
|
-
|
|
880
|
+
align-items: center;
|
|
884
881
|
gap: var(--grim-space-3);
|
|
885
882
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
883
|
+
.chips.kind-chips {
|
|
884
|
+
flex-shrink: 0;
|
|
885
|
+
}
|
|
886
|
+
/* The one child that absorbs the row's slack — which is also what
|
|
887
|
+
pushes the overflow menu and the deprecated toggle to the far end,
|
|
888
|
+
without an `auto` margin that would put them out of reach once the
|
|
889
|
+
row does overflow.
|
|
890
|
+
|
|
891
|
+
`hidden`, never `auto`: a scrollbar here put the chips behind a
|
|
892
|
+
gesture nobody looks for and, worse, let the rail grow past the row
|
|
893
|
+
so the overflow menu it was supposed to sit beside was pushed off
|
|
894
|
+
the end entirely. What does not fit is decided by measurement (see
|
|
895
|
+
`railFit`) and drawn as nothing. */
|
|
896
|
+
.chips.kw-rail {
|
|
897
|
+
flex: 1 1 auto;
|
|
898
|
+
min-width: 0;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
}
|
|
901
|
+
/* Past the measured fit. `visibility`, not `display`: the chip keeps
|
|
902
|
+
its box, so the measurement that excluded it stays true instead of
|
|
903
|
+
freeing the width that would let it back in. It is in the overflow
|
|
904
|
+
menu, which is where the reader reaches it. */
|
|
905
|
+
.chip.kw.clipped {
|
|
906
|
+
visibility: hidden;
|
|
907
|
+
}
|
|
908
|
+
.filter-divider {
|
|
909
|
+
flex-shrink: 0;
|
|
890
910
|
align-self: stretch;
|
|
891
|
-
width:
|
|
911
|
+
width: var(--grim-border-width);
|
|
892
912
|
background: var(--grim-color-border);
|
|
893
913
|
}
|
|
894
|
-
/*
|
|
895
|
-
|
|
896
|
-
|
|
914
|
+
/* Answers a different question from sort and kind — what the catalog
|
|
915
|
+
is withholding, not how to arrange it — so it sits apart, at the
|
|
916
|
+
end of the row. */
|
|
897
917
|
.chip.deprecated-toggle {
|
|
898
|
-
|
|
918
|
+
flex-shrink: 0;
|
|
919
|
+
}
|
|
920
|
+
.kw-menu {
|
|
921
|
+
flex-shrink: 0;
|
|
922
|
+
}
|
|
923
|
+
/* Row three: the answer on the left, the controls that arrange it on
|
|
924
|
+
the right. */
|
|
925
|
+
.meta-row {
|
|
926
|
+
display: flex;
|
|
927
|
+
align-items: center;
|
|
928
|
+
gap: var(--grim-space-4);
|
|
929
|
+
flex-wrap: wrap;
|
|
930
|
+
/* On top of the stack's own gap: it takes this row away from the
|
|
931
|
+
filters and leaves it sitting on the list it describes. */
|
|
932
|
+
margin-top: var(--grim-space-5);
|
|
933
|
+
}
|
|
934
|
+
/* Bottom-aligned, not centred: it is a caption for the list below
|
|
935
|
+
it, and centring it against two bordered controls left it floating
|
|
936
|
+
between the two rows belonging to neither. */
|
|
937
|
+
.result-count {
|
|
938
|
+
margin: 0;
|
|
939
|
+
margin-right: auto;
|
|
940
|
+
align-self: end;
|
|
941
|
+
color: var(--grim-color-muted);
|
|
942
|
+
font-size: var(--grim-text-sm);
|
|
899
943
|
}
|
|
900
944
|
/* Pressed state borrows the deprecation colour rather than the
|
|
901
945
|
accent, so "deprecated entries are in view" is signalled by the
|
|
@@ -905,20 +949,81 @@ function repoLabel(url: string): string {
|
|
|
905
949
|
border-color: var(--grim-color-deprecated);
|
|
906
950
|
background: var(--grim-color-chip-bg);
|
|
907
951
|
}
|
|
952
|
+
/* One object with two halves — a direction cap and the field —
|
|
953
|
+
sharing an edge, and built to the search field's own measurements
|
|
954
|
+
so the pair sits level with it at the same height. Squarer than a
|
|
955
|
+
chip on purpose: a chip toggles a filter, this chooses a value,
|
|
956
|
+
and it belongs to the field beside it rather than to the row of
|
|
957
|
+
filters below. */
|
|
958
|
+
.sort-group {
|
|
959
|
+
display: flex;
|
|
960
|
+
align-items: stretch;
|
|
961
|
+
flex: 0 0 auto;
|
|
962
|
+
}
|
|
963
|
+
/* Sized to their own row, not to the search field. They used to sit
|
|
964
|
+
beside it and had to match its height to read as one strip; on a
|
|
965
|
+
line of their own that height is just bulk. */
|
|
966
|
+
.sort-dir,
|
|
967
|
+
.sort-field {
|
|
968
|
+
position: relative;
|
|
969
|
+
padding: var(--grim-space-2) var(--grim-space-4);
|
|
970
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
971
|
+
background: var(--grim-color-card);
|
|
972
|
+
color: var(--grim-color-fg);
|
|
973
|
+
font-family: inherit;
|
|
974
|
+
font-size: var(--grim-text-sm);
|
|
975
|
+
cursor: pointer;
|
|
976
|
+
}
|
|
977
|
+
.sort-dir {
|
|
978
|
+
display: inline-flex;
|
|
979
|
+
align-items: center;
|
|
980
|
+
border-radius: var(--grim-radius-control) 0 0 var(--grim-radius-control);
|
|
981
|
+
}
|
|
982
|
+
/* Pulled onto its neighbour's border so the seam is one line, not a
|
|
983
|
+
doubled one. Derived from the token, never a hand-written -1px. */
|
|
984
|
+
.sort-field {
|
|
985
|
+
border-radius: 0 var(--grim-radius-control) var(--grim-radius-control) 0;
|
|
986
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
987
|
+
}
|
|
988
|
+
.sort-dir:hover,
|
|
989
|
+
.sort-field:hover {
|
|
990
|
+
border-color: var(--grim-color-accent);
|
|
991
|
+
}
|
|
992
|
+
/* Whichever half is hovered or focused owns the shared edge. */
|
|
993
|
+
.sort-dir:hover,
|
|
994
|
+
.sort-field:hover,
|
|
995
|
+
.sort-dir:focus-visible,
|
|
996
|
+
.sort-field:focus-visible {
|
|
997
|
+
z-index: 1;
|
|
998
|
+
}
|
|
999
|
+
.sort-dir:focus-visible,
|
|
1000
|
+
.sort-field:focus-visible {
|
|
1001
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1002
|
+
outline-offset: 1px;
|
|
1003
|
+
}
|
|
1004
|
+
/* One box for every chip in the filter row — the kinds, the keywords,
|
|
1005
|
+
the deprecated toggle and the overflow menu's trigger alike. The
|
|
1006
|
+
line-height is stated rather than inherited because a `<summary>`
|
|
1007
|
+
and a `<button>` do not agree on their own, and a row of chips that
|
|
1008
|
+
disagree by a pixel reads as sloppy rather than as a group. */
|
|
908
1009
|
.chip {
|
|
1010
|
+
display: inline-flex;
|
|
1011
|
+
align-items: center;
|
|
909
1012
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
910
1013
|
background: var(--grim-color-chip-bg);
|
|
911
1014
|
color: var(--grim-color-fg);
|
|
912
1015
|
border-radius: var(--grim-radius-pill);
|
|
913
1016
|
padding: var(--grim-space-2) var(--grim-space-5);
|
|
914
1017
|
font-size: var(--grim-text-sm);
|
|
1018
|
+
line-height: 1.4;
|
|
1019
|
+
white-space: nowrap;
|
|
915
1020
|
transition: border-color var(--grim-duration-base) ease;
|
|
916
1021
|
cursor: pointer;
|
|
917
1022
|
}
|
|
918
1023
|
.chip small {
|
|
919
1024
|
color: var(--grim-color-muted);
|
|
920
1025
|
}
|
|
921
|
-
/* button-only:
|
|
1026
|
+
/* button-only: a chip rendered as a span is inert and takes no hover */
|
|
922
1027
|
button.chip:hover {
|
|
923
1028
|
border-color: var(--grim-color-accent);
|
|
924
1029
|
}
|
|
@@ -926,6 +1031,174 @@ function repoLabel(url: string): string {
|
|
|
926
1031
|
border-color: var(--grim-color-accent);
|
|
927
1032
|
background: var(--grim-color-accent-soft);
|
|
928
1033
|
}
|
|
1034
|
+
/* The keyword rail reorders on every click — it is rescored against
|
|
1035
|
+
what is on screen — and the chips slide rather than jump. The FLIP
|
|
1036
|
+
effect in `Catalog.tsx` sets an inline `translate` back to where
|
|
1037
|
+
each chip was and drops it on the next frame; this transition is
|
|
1038
|
+
what plays that back. Without a declared duration the invert would
|
|
1039
|
+
simply stick. */
|
|
1040
|
+
.chip.kw {
|
|
1041
|
+
transition:
|
|
1042
|
+
border-color var(--grim-duration-base) ease,
|
|
1043
|
+
translate var(--grim-duration-slow) ease-out;
|
|
1044
|
+
}
|
|
1045
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1046
|
+
.chip.kw {
|
|
1047
|
+
transition: border-color var(--grim-duration-base) ease;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/* The overflow menu. A `<details>` for the same reasons the platform
|
|
1051
|
+
picker is one — it opens, closes and takes Escape without a line of
|
|
1052
|
+
script — and the panel is the `.os-menu` popover's measurements so
|
|
1053
|
+
the two read as the same object in two places. */
|
|
1054
|
+
.kw-menu {
|
|
1055
|
+
position: relative;
|
|
1056
|
+
display: flex;
|
|
1057
|
+
}
|
|
1058
|
+
/* The trigger wears `.chip`, so its box comes from there and cannot
|
|
1059
|
+
drift a pixel off the chips it sits beside. Only what a chip does
|
|
1060
|
+
not cover is stated here. */
|
|
1061
|
+
.kw-menu > summary {
|
|
1062
|
+
display: inline-flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
color: var(--grim-color-muted);
|
|
1065
|
+
/* Both spellings: the default triangle is a marker in Firefox and
|
|
1066
|
+
a `::-webkit-details-marker` pseudo-element in older WebKit. */
|
|
1067
|
+
list-style: none;
|
|
1068
|
+
}
|
|
1069
|
+
.kw-menu > summary::-webkit-details-marker {
|
|
1070
|
+
display: none;
|
|
1071
|
+
}
|
|
1072
|
+
.kw-menu > summary:hover,
|
|
1073
|
+
.kw-menu > summary:focus-visible,
|
|
1074
|
+
.kw-menu[open] > summary {
|
|
1075
|
+
border-color: var(--grim-color-accent);
|
|
1076
|
+
color: var(--grim-color-fg);
|
|
1077
|
+
}
|
|
1078
|
+
.kw-menu-panel {
|
|
1079
|
+
position: absolute;
|
|
1080
|
+
top: calc(100% + var(--grim-space-2));
|
|
1081
|
+
left: 0;
|
|
1082
|
+
z-index: 5;
|
|
1083
|
+
display: flex;
|
|
1084
|
+
flex-direction: column;
|
|
1085
|
+
gap: var(--grim-space-3);
|
|
1086
|
+
/* The panel's own measurement: wide enough for the longest keyword
|
|
1087
|
+
a publisher is likely to write plus its count, and capped so a
|
|
1088
|
+
long vocabulary scrolls instead of running off the viewport. */
|
|
1089
|
+
width: 15rem;
|
|
1090
|
+
padding: var(--grim-space-3);
|
|
1091
|
+
background: var(--grim-color-card);
|
|
1092
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1093
|
+
border-radius: var(--grim-radius-control);
|
|
1094
|
+
box-shadow: var(--grim-shadow-raised);
|
|
1095
|
+
}
|
|
1096
|
+
.kw-menu-search {
|
|
1097
|
+
width: 100%;
|
|
1098
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1099
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1100
|
+
border-radius: var(--grim-radius-inset);
|
|
1101
|
+
background: var(--grim-color-bg);
|
|
1102
|
+
color: var(--grim-color-fg);
|
|
1103
|
+
font: inherit;
|
|
1104
|
+
font-size: var(--grim-text-sm);
|
|
1105
|
+
}
|
|
1106
|
+
.kw-menu-search:focus-visible {
|
|
1107
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1108
|
+
outline-offset: 1px;
|
|
1109
|
+
}
|
|
1110
|
+
.kw-menu-list {
|
|
1111
|
+
display: flex;
|
|
1112
|
+
flex-direction: column;
|
|
1113
|
+
/* Ten rows and then a scrollbar — a measured height, not a rhythm
|
|
1114
|
+
step: it is the row's own height times the count that still reads
|
|
1115
|
+
as a list rather than a page. */
|
|
1116
|
+
max-height: 15rem;
|
|
1117
|
+
overflow-y: auto;
|
|
1118
|
+
}
|
|
1119
|
+
.kw-menu-item {
|
|
1120
|
+
display: flex;
|
|
1121
|
+
align-items: center;
|
|
1122
|
+
justify-content: space-between;
|
|
1123
|
+
gap: var(--grim-space-4);
|
|
1124
|
+
width: 100%;
|
|
1125
|
+
border: none;
|
|
1126
|
+
border-radius: var(--grim-radius-inset);
|
|
1127
|
+
background: none;
|
|
1128
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1129
|
+
color: var(--grim-color-fg);
|
|
1130
|
+
font: inherit;
|
|
1131
|
+
font-size: var(--grim-text-sm);
|
|
1132
|
+
text-align: left;
|
|
1133
|
+
cursor: pointer;
|
|
1134
|
+
}
|
|
1135
|
+
.kw-menu-item:hover,
|
|
1136
|
+
.kw-menu-item:focus-visible {
|
|
1137
|
+
background: var(--grim-color-chip-bg);
|
|
1138
|
+
}
|
|
1139
|
+
.kw-menu-item small {
|
|
1140
|
+
color: var(--grim-color-muted);
|
|
1141
|
+
}
|
|
1142
|
+
.kw-menu-empty {
|
|
1143
|
+
margin: 0;
|
|
1144
|
+
padding: var(--grim-space-3) var(--grim-space-4);
|
|
1145
|
+
color: var(--grim-color-muted);
|
|
1146
|
+
font-size: var(--grim-text-sm);
|
|
1147
|
+
}
|
|
1148
|
+
/* Cards or list, beside the sort control because it answers the same
|
|
1149
|
+
question — how the catalog is arranged. Built to the sort group's
|
|
1150
|
+
measurements, and the second button is pulled onto the first's
|
|
1151
|
+
border so the seam is one line rather than a doubled one. */
|
|
1152
|
+
.view-toggle {
|
|
1153
|
+
display: flex;
|
|
1154
|
+
align-items: stretch;
|
|
1155
|
+
flex: 0 0 auto;
|
|
1156
|
+
/* The row centres its items, which left these two shorter than the
|
|
1157
|
+
search field and the sort control beside them: a flex container
|
|
1158
|
+
raises no strut, so a button holding nothing but a 15px icon is
|
|
1159
|
+
15px tall plus its padding, while the select next to it is a text
|
|
1160
|
+
line tall. Stretching to the line's own height is what makes the
|
|
1161
|
+
three agree without any of them stating a height — `.sort-group`
|
|
1162
|
+
gets the same result from its own `align-items: stretch`, since
|
|
1163
|
+
the select inside it is the tall one. */
|
|
1164
|
+
align-self: stretch;
|
|
1165
|
+
}
|
|
1166
|
+
.view-pick {
|
|
1167
|
+
position: relative;
|
|
1168
|
+
display: inline-flex;
|
|
1169
|
+
align-items: center;
|
|
1170
|
+
justify-content: center;
|
|
1171
|
+
padding: var(--grim-space-2) var(--grim-space-4);
|
|
1172
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1173
|
+
background: var(--grim-color-card);
|
|
1174
|
+
color: var(--grim-color-muted);
|
|
1175
|
+
cursor: pointer;
|
|
1176
|
+
}
|
|
1177
|
+
.view-pick:first-child {
|
|
1178
|
+
border-radius: var(--grim-radius-control) 0 0 var(--grim-radius-control);
|
|
1179
|
+
}
|
|
1180
|
+
.view-pick:last-child {
|
|
1181
|
+
border-radius: 0 var(--grim-radius-control) var(--grim-radius-control) 0;
|
|
1182
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
1183
|
+
}
|
|
1184
|
+
.view-pick:hover {
|
|
1185
|
+
border-color: var(--grim-color-accent);
|
|
1186
|
+
}
|
|
1187
|
+
.view-pick.active {
|
|
1188
|
+
border-color: var(--grim-color-accent);
|
|
1189
|
+
background: var(--grim-color-accent-soft);
|
|
1190
|
+
color: var(--grim-color-fg);
|
|
1191
|
+
}
|
|
1192
|
+
/* Whichever half is hovered, focused or current owns the shared edge. */
|
|
1193
|
+
.view-pick:hover,
|
|
1194
|
+
.view-pick:focus-visible,
|
|
1195
|
+
.view-pick.active {
|
|
1196
|
+
z-index: 1;
|
|
1197
|
+
}
|
|
1198
|
+
.view-pick:focus-visible {
|
|
1199
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1200
|
+
outline-offset: 1px;
|
|
1201
|
+
}
|
|
929
1202
|
|
|
930
1203
|
.grid {
|
|
931
1204
|
list-style: none;
|
|
@@ -938,11 +1211,17 @@ function repoLabel(url: string): string {
|
|
|
938
1211
|
.card {
|
|
939
1212
|
display: flex;
|
|
940
1213
|
flex-direction: column;
|
|
941
|
-
gap: var(--grim-space-
|
|
1214
|
+
gap: var(--grim-space-5);
|
|
942
1215
|
background: var(--grim-color-card);
|
|
943
1216
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
944
|
-
border-radius: var(--grim-radius-
|
|
1217
|
+
border-radius: var(--grim-radius-surface);
|
|
945
1218
|
padding: var(--grim-space-6) var(--grim-space-6);
|
|
1219
|
+
/* Crops the kind watermark to the card's rounded edge, and gives
|
|
1220
|
+
it a stacking context to sit behind in. Without `isolation` a
|
|
1221
|
+
`z-index: -1` child escapes to the root and disappears under the
|
|
1222
|
+
card's own background instead of showing through it. */
|
|
1223
|
+
overflow: hidden;
|
|
1224
|
+
isolation: isolate;
|
|
946
1225
|
/* Whole card navigates to the detail page via the stretched title
|
|
947
1226
|
link below; real controls inside re-stack above the overlay. */
|
|
948
1227
|
position: relative;
|
|
@@ -951,51 +1230,126 @@ function repoLabel(url: string): string {
|
|
|
951
1230
|
}
|
|
952
1231
|
/* Card highlight yields while an inner control is hovered, so its
|
|
953
1232
|
own hover state reads as the click target instead. */
|
|
954
|
-
.card:hover:not(
|
|
955
|
-
:has(.keywords :hover, .copy-group :hover, .source:hover)
|
|
956
|
-
) {
|
|
1233
|
+
.card:hover:not(:has(.keywords :hover, .copy-group :hover, .rating-group a:hover)) {
|
|
957
1234
|
border-color: var(--grim-color-accent);
|
|
958
1235
|
}
|
|
959
|
-
/* Keyboard focus
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1236
|
+
/* Keyboard focus reads as a thicker edge, not a second decoration:
|
|
1237
|
+
the border takes the accent and a 2px outline sits directly
|
|
1238
|
+
outside it, so the two merge into one ring that follows the card's
|
|
1239
|
+
radius. Matched to the list view's focus ring, which is the same
|
|
1240
|
+
2px — a card and a row are the same Tab stop in two shapes, and a
|
|
1241
|
+
focus indicator that changes weight between them reads as two
|
|
1242
|
+
different states. */
|
|
963
1243
|
.card:focus-visible {
|
|
964
|
-
outline:
|
|
1244
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1245
|
+
outline-offset: 0;
|
|
965
1246
|
border-color: var(--grim-color-accent);
|
|
966
1247
|
}
|
|
1248
|
+
/* Makes the whole card the package link. Everything interactive
|
|
1249
|
+
inside it therefore has to be lifted above this — `z-index: 0`
|
|
1250
|
+
here, `1` on each control, so the contract is two stated numbers
|
|
1251
|
+
rather than a tree-order tie that a markup edit can reverse. */
|
|
967
1252
|
.card h2 a::after {
|
|
968
1253
|
content: "";
|
|
969
1254
|
position: absolute;
|
|
970
1255
|
inset: 0;
|
|
1256
|
+
z-index: 0;
|
|
971
1257
|
}
|
|
972
1258
|
.card .keywords,
|
|
973
|
-
.card .copy-group
|
|
974
|
-
.card .source {
|
|
1259
|
+
.card .copy-group {
|
|
975
1260
|
position: relative;
|
|
976
1261
|
}
|
|
1262
|
+
/* Two rows — the name, then the address — with the logo spanning
|
|
1263
|
+
both down the left. A grid rather than the flex row it was: the
|
|
1264
|
+
name has to be able to shrink to nothing so it can ellipsis, and
|
|
1265
|
+
the address has to line up under it rather than under the logo.
|
|
1266
|
+
Nothing shares those rows: every badge that tried cost the name
|
|
1267
|
+
its width on a narrow card. They live in `.card-tags` below. */
|
|
977
1268
|
.card-head {
|
|
978
|
-
display:
|
|
1269
|
+
display: grid;
|
|
1270
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
979
1271
|
align-items: center;
|
|
980
|
-
gap: var(--grim-space-4);
|
|
1272
|
+
column-gap: var(--grim-space-4);
|
|
1273
|
+
}
|
|
1274
|
+
.card-head .card-logo {
|
|
1275
|
+
grid-row: 1 / span 2;
|
|
1276
|
+
}
|
|
1277
|
+
/* The address line runs the full width under the name: the kind is
|
|
1278
|
+
a word on it now, not a badge in the head, so nothing sits to its
|
|
1279
|
+
right to make room for. */
|
|
1280
|
+
.card-head .namespace {
|
|
1281
|
+
grid-column: 2 / -1;
|
|
1282
|
+
grid-row: 2;
|
|
1283
|
+
}
|
|
1284
|
+
.card-head h2 {
|
|
1285
|
+
grid-column: 2;
|
|
1286
|
+
grid-row: 1;
|
|
1287
|
+
}
|
|
1288
|
+
/* Level with the name, in the card's top-right corner. The only
|
|
1289
|
+
thing that shares this row — the kind badge that used to sit here
|
|
1290
|
+
went down to the address line, which is what stopped the name
|
|
1291
|
+
being squeezed between two of them. */
|
|
1292
|
+
.card-head .rating-group {
|
|
1293
|
+
grid-column: 3;
|
|
1294
|
+
grid-row: 1;
|
|
1295
|
+
justify-self: end;
|
|
1296
|
+
}
|
|
1297
|
+
/* Texture, not an icon: oversized and hung off the card's own
|
|
1298
|
+
bottom-right corner, so `overflow: hidden` clips it to the rounded
|
|
1299
|
+
edge and only part of the glyph is on the card at all.
|
|
1300
|
+
The overhang is a share of the glyph, not a length: `translate`
|
|
1301
|
+
resolves its percentages against the element's own box, so the
|
|
1302
|
+
card and the detail panel hang their marks out by the same
|
|
1303
|
+
fraction even though the two are drawn at different sizes. An
|
|
1304
|
+
inset percentage would not — those resolve against the container,
|
|
1305
|
+
which is a different width on each page. */
|
|
1306
|
+
.card-watermark {
|
|
1307
|
+
position: absolute;
|
|
1308
|
+
z-index: -1;
|
|
1309
|
+
/* On the element, never on the stroke colour. The glyph is
|
|
1310
|
+
overlapping stroked paths; a translucent colour composites each
|
|
1311
|
+
crossing twice and leaves the joins visibly darker than the
|
|
1312
|
+
lines they join. Opacity here rasterizes the whole glyph at full
|
|
1313
|
+
strength and blends that once, so the tint is flat. */
|
|
1314
|
+
opacity: 0.14;
|
|
1315
|
+
bottom: 0;
|
|
1316
|
+
right: 0;
|
|
1317
|
+
transform: translate(26%, 26%);
|
|
1318
|
+
pointer-events: none;
|
|
981
1319
|
}
|
|
982
1320
|
.card h2 {
|
|
983
|
-
flex: 1;
|
|
984
1321
|
min-width: 0;
|
|
985
1322
|
margin: 0;
|
|
986
1323
|
font-size: var(--grim-text-lg);
|
|
1324
|
+
/* Tight, so the address sits under the name rather than a line
|
|
1325
|
+
away from it — the two are one label in two parts, and the
|
|
1326
|
+
body's reading leading put a paragraph break between them. */
|
|
1327
|
+
line-height: 1.2;
|
|
987
1328
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
988
|
-
word-break: break-word;
|
|
989
1329
|
}
|
|
1330
|
+
/* One line, always. A package name is a single token, so wrapping it
|
|
1331
|
+
gave a two-line head on a long name and a ragged grid; the ellipsis
|
|
1332
|
+
keeps every card's head the same height, and the full name is on
|
|
1333
|
+
the page this links to. Colour and underline come from the card,
|
|
1334
|
+
not from the link default — the whole card is the link, so the
|
|
1335
|
+
name is a heading that happens to be clickable. */
|
|
990
1336
|
.card h2 a {
|
|
1337
|
+
display: block;
|
|
1338
|
+
overflow: hidden;
|
|
1339
|
+
text-overflow: ellipsis;
|
|
1340
|
+
white-space: nowrap;
|
|
991
1341
|
color: inherit;
|
|
992
1342
|
text-decoration: none;
|
|
993
1343
|
}
|
|
1344
|
+
/* Geometry, not a scale step: the tile is sized to the two rows it
|
|
1345
|
+
spans — the name line plus the address line under it — so it reads
|
|
1346
|
+
as one block beside them rather than a badge floating against the
|
|
1347
|
+
first. `.logo-mark` is a percentage of this, so the placeholder
|
|
1348
|
+
and the broken glyph follow on their own. */
|
|
994
1349
|
.card-logo {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
border-radius: var(--grim-radius-md);
|
|
1350
|
+
width: 36px;
|
|
1351
|
+
height: 36px;
|
|
1352
|
+
border-radius: var(--grim-radius-inset);
|
|
999
1353
|
}
|
|
1000
1354
|
img.card-logo {
|
|
1001
1355
|
object-fit: contain;
|
|
@@ -1007,7 +1361,8 @@ function repoLabel(url: string): string {
|
|
|
1007
1361
|
justify-content: center;
|
|
1008
1362
|
color: var(--grim-color-on-accent);
|
|
1009
1363
|
font-weight: 700;
|
|
1010
|
-
|
|
1364
|
+
/* Up a step with the tile: `sm` left the letter adrift in 40px. */
|
|
1365
|
+
font-size: var(--grim-text-lg);
|
|
1011
1366
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1012
1367
|
}
|
|
1013
1368
|
/* THE LOGO SLOT — one box, three states, no layout shift.
|
|
@@ -1072,6 +1427,207 @@ function repoLabel(url: string): string {
|
|
|
1072
1427
|
transition: none;
|
|
1073
1428
|
}
|
|
1074
1429
|
}
|
|
1430
|
+
/* The list view: a grid of row anchors, not a `<table>`.
|
|
1431
|
+
No headers, deliberately — a header row that cannot sort is a
|
|
1432
|
+
header row that looks like it sorts, and sorting lives in the
|
|
1433
|
+
toolbar. With the headers gone there are no column semantics left
|
|
1434
|
+
to justify the element, and what remains is a list of links.
|
|
1435
|
+
`subgrid` keeps the columns aligned without the markup.
|
|
1436
|
+
|
|
1437
|
+
The container owns the border, the radius and the clip; the rows
|
|
1438
|
+
own the rules between them. That is what makes the corners round
|
|
1439
|
+
in every state — a row's hover fill and focus ring are clipped to
|
|
1440
|
+
the container rather than cutting square across it. */
|
|
1441
|
+
.table {
|
|
1442
|
+
display: grid;
|
|
1443
|
+
/* logo | kind | name | description | updated. The two fixed-width
|
|
1444
|
+
marks lead, so the names start on one line down the whole list
|
|
1445
|
+
and the eye runs the column it actually reads. The name is
|
|
1446
|
+
capped rather than `max-content`: an unbounded name ate the
|
|
1447
|
+
description's space, and past the cap both clip instead. The
|
|
1448
|
+
description is the only track that grows. */
|
|
1449
|
+
grid-template-columns: auto auto minmax(8rem, 14rem) 1fr auto;
|
|
1450
|
+
background: var(--grim-color-card);
|
|
1451
|
+
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1452
|
+
/* `xl`, the content-surface step — the same one the card wears.
|
|
1453
|
+
The two are one list in two shapes, so a reader switching between
|
|
1454
|
+
them should see the corner stay put. It was `lg`, the control
|
|
1455
|
+
step, which put the table's corners with the sort pair's rather
|
|
1456
|
+
than with the cards'. */
|
|
1457
|
+
border-radius: var(--grim-radius-surface);
|
|
1458
|
+
overflow: hidden;
|
|
1459
|
+
}
|
|
1460
|
+
/* One more track when the index publishes ratings. Decided once per
|
|
1461
|
+
index, never per filter — see `PackageTable`. */
|
|
1462
|
+
.table.rated {
|
|
1463
|
+
grid-template-columns: auto auto minmax(8rem, 14rem) 1fr auto auto;
|
|
1464
|
+
}
|
|
1465
|
+
.table .row {
|
|
1466
|
+
grid-column: 1 / -1;
|
|
1467
|
+
display: grid;
|
|
1468
|
+
grid-template-columns: subgrid;
|
|
1469
|
+
align-items: center;
|
|
1470
|
+
gap: var(--grim-space-5);
|
|
1471
|
+
padding: var(--grim-space-3) var(--grim-space-5);
|
|
1472
|
+
color: inherit;
|
|
1473
|
+
text-decoration: none;
|
|
1474
|
+
font-size: var(--grim-text-sm);
|
|
1475
|
+
transition: background-color var(--grim-duration-base) ease;
|
|
1476
|
+
}
|
|
1477
|
+
/* The rule belongs between rows, not under the last one — which the
|
|
1478
|
+
container's own border already draws. */
|
|
1479
|
+
.table .row + .row {
|
|
1480
|
+
border-top: var(--grim-border-width) solid var(--grim-color-border);
|
|
1481
|
+
}
|
|
1482
|
+
.table .row:hover {
|
|
1483
|
+
background: var(--grim-color-chip-bg);
|
|
1484
|
+
}
|
|
1485
|
+
/* Inset, so the ring stays inside the row: an outline sits outside
|
|
1486
|
+
the border box and the container's `overflow: hidden` would clip
|
|
1487
|
+
it to a stray line on the first and last rows. Pulled in by its
|
|
1488
|
+
own width, which is the a11y floor rather than a style. */
|
|
1489
|
+
.table .row:focus-visible {
|
|
1490
|
+
outline: 2px solid var(--grim-color-accent);
|
|
1491
|
+
outline-offset: -2px;
|
|
1492
|
+
background: var(--grim-color-accent-soft);
|
|
1493
|
+
}
|
|
1494
|
+
/* The two rows that meet the container's corners carry its radius,
|
|
1495
|
+
so a hover fill or a focus ring on either follows the rounding
|
|
1496
|
+
instead of cutting straight across it. */
|
|
1497
|
+
.table .row:first-child {
|
|
1498
|
+
border-start-start-radius: var(--grim-radius-surface);
|
|
1499
|
+
border-start-end-radius: var(--grim-radius-surface);
|
|
1500
|
+
}
|
|
1501
|
+
.table .row:last-child {
|
|
1502
|
+
border-end-start-radius: var(--grim-radius-surface);
|
|
1503
|
+
border-end-end-radius: var(--grim-radius-surface);
|
|
1504
|
+
}
|
|
1505
|
+
/* Exactly one line per cell, always. Even rows are the whole reason
|
|
1506
|
+
to prefer this view to the cards: a cell that wraps makes its row
|
|
1507
|
+
taller than its neighbours, and a column the eye can no longer run
|
|
1508
|
+
down is a list doing the cards' job badly. */
|
|
1509
|
+
.table .row > * {
|
|
1510
|
+
min-width: 0;
|
|
1511
|
+
white-space: nowrap;
|
|
1512
|
+
overflow: hidden;
|
|
1513
|
+
text-overflow: ellipsis;
|
|
1514
|
+
/* Every cell is one text line and no cell is a flex box, so all
|
|
1515
|
+
five have the same height and the same baseline — the icons ride
|
|
1516
|
+
that line on `vertical-align` rather than being centred as boxes
|
|
1517
|
+
of their own. Cell-by-cell centring is what left the marks and
|
|
1518
|
+
the words a pixel apart; this makes them the same line. It is
|
|
1519
|
+
also what keeps `text-overflow` working, which a flex container
|
|
1520
|
+
would have silently dropped. */
|
|
1521
|
+
line-height: 1.6;
|
|
1522
|
+
}
|
|
1523
|
+
/* One glyph wide, and the same width whatever the kind is — so every
|
|
1524
|
+
name beside it starts on one line down the whole list. The colour
|
|
1525
|
+
is set inline, from the kind's own token or the deprecation one. */
|
|
1526
|
+
.t-kind {
|
|
1527
|
+
text-align: center;
|
|
1528
|
+
}
|
|
1529
|
+
.t-kind svg {
|
|
1530
|
+
vertical-align: middle;
|
|
1531
|
+
}
|
|
1532
|
+
/* The card's tile at a row's height. Geometry, not a scale step, and
|
|
1533
|
+
measured against the text line beside it rather than against the
|
|
1534
|
+
kind mark: 20px is the tile that reads as belonging to the name
|
|
1535
|
+
without making the row taller than one line of type. `.logo-mark`
|
|
1536
|
+
and the fallback letter are proportions of the tile, so both follow
|
|
1537
|
+
on their own — only the letter needs a step down, since the card's
|
|
1538
|
+
is sized for a 36px tile. */
|
|
1539
|
+
.t-logo .card-logo {
|
|
1540
|
+
width: 20px;
|
|
1541
|
+
height: 20px;
|
|
1542
|
+
}
|
|
1543
|
+
.t-logo .card-logo-fallback {
|
|
1544
|
+
font-size: var(--grim-text-2xs);
|
|
1545
|
+
}
|
|
1546
|
+
/* The fallback for a kind this renderer has no mark for. */
|
|
1547
|
+
.t-kind-word {
|
|
1548
|
+
font-size: var(--grim-text-2xs);
|
|
1549
|
+
color: var(--grim-color-muted);
|
|
1550
|
+
}
|
|
1551
|
+
.t-name {
|
|
1552
|
+
color: var(--grim-color-fg);
|
|
1553
|
+
font-weight: 600;
|
|
1554
|
+
}
|
|
1555
|
+
.t-desc,
|
|
1556
|
+
.t-updated {
|
|
1557
|
+
color: var(--grim-color-muted);
|
|
1558
|
+
}
|
|
1559
|
+
/* Last column, held at the right edge: the count, then its arrow.
|
|
1560
|
+
The two sit next to each other rather than at opposite sides of a
|
|
1561
|
+
wide cell — they are one reading, and the gap that separated them
|
|
1562
|
+
was the cell's width showing through. */
|
|
1563
|
+
.t-rating {
|
|
1564
|
+
text-align: right;
|
|
1565
|
+
color: var(--grim-color-muted);
|
|
1566
|
+
}
|
|
1567
|
+
.t-rating svg {
|
|
1568
|
+
vertical-align: middle;
|
|
1569
|
+
margin-left: var(--grim-space-2);
|
|
1570
|
+
/* Optical, not rhythm: the glyph's ink sits high in its own box, so
|
|
1571
|
+
the middle of the box reads above the digits beside it. A pixel
|
|
1572
|
+
down puts the two on the same visual line. */
|
|
1573
|
+
translate: 0 1px;
|
|
1574
|
+
}
|
|
1575
|
+
/* The count carries the column's alignment, and the arrow after it
|
|
1576
|
+
inherits it: a fixed box, right-aligned, in tabular figures. Without
|
|
1577
|
+
the fixed box a three-digit count would push its own arrow right of
|
|
1578
|
+
the one above it. Geometry — it holds four digits at this step. */
|
|
1579
|
+
.t-votes {
|
|
1580
|
+
display: inline-block;
|
|
1581
|
+
width: 2rem;
|
|
1582
|
+
text-align: right;
|
|
1583
|
+
font-variant-numeric: tabular-nums;
|
|
1584
|
+
}
|
|
1585
|
+
/* Every cell names its own column rather than being auto-placed.
|
|
1586
|
+
Auto-placement fills tracks in document order, so the moment a cell
|
|
1587
|
+
is dropped — which the narrow layout below does to the description
|
|
1588
|
+
— every cell after it slides one track left and the columns stop
|
|
1589
|
+
meaning what they did. Naming them makes dropping one safe. */
|
|
1590
|
+
.t-logo {
|
|
1591
|
+
grid-column: 1;
|
|
1592
|
+
}
|
|
1593
|
+
.t-kind {
|
|
1594
|
+
grid-column: 2;
|
|
1595
|
+
}
|
|
1596
|
+
.t-name {
|
|
1597
|
+
grid-column: 3;
|
|
1598
|
+
}
|
|
1599
|
+
.t-desc {
|
|
1600
|
+
grid-column: 4;
|
|
1601
|
+
}
|
|
1602
|
+
.t-updated {
|
|
1603
|
+
grid-column: 5;
|
|
1604
|
+
}
|
|
1605
|
+
.t-rating {
|
|
1606
|
+
grid-column: 6;
|
|
1607
|
+
}
|
|
1608
|
+
/* Narrow viewports lose the description rather than scroll sideways:
|
|
1609
|
+
it is context for the name beside it, and the detail page states it
|
|
1610
|
+
in full. The remaining cells move up a track with it, so no empty
|
|
1611
|
+
column and no stray gap is left behind. */
|
|
1612
|
+
@media (max-width: 40rem) {
|
|
1613
|
+
.table,
|
|
1614
|
+
.table.rated {
|
|
1615
|
+
grid-template-columns: auto auto minmax(6rem, 1fr) auto;
|
|
1616
|
+
}
|
|
1617
|
+
.table.rated {
|
|
1618
|
+
grid-template-columns: auto auto minmax(6rem, 1fr) auto auto;
|
|
1619
|
+
}
|
|
1620
|
+
.t-desc {
|
|
1621
|
+
display: none;
|
|
1622
|
+
}
|
|
1623
|
+
.t-updated {
|
|
1624
|
+
grid-column: 4;
|
|
1625
|
+
}
|
|
1626
|
+
.t-rating {
|
|
1627
|
+
grid-column: 5;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1075
1631
|
.badge {
|
|
1076
1632
|
flex-shrink: 0;
|
|
1077
1633
|
font-size: var(--grim-text-2xs);
|
|
@@ -1106,78 +1662,180 @@ function repoLabel(url: string): string {
|
|
|
1106
1662
|
.chip[class*="kind-"] {
|
|
1107
1663
|
color: var(--grim-color-fg);
|
|
1108
1664
|
}
|
|
1665
|
+
/* A step below the name and quieter: where the package lives is
|
|
1666
|
+
context for the name above it, not a second heading. */
|
|
1109
1667
|
.namespace {
|
|
1110
1668
|
margin: 0;
|
|
1111
1669
|
color: var(--grim-color-muted);
|
|
1112
|
-
font-size: var(--grim-text-
|
|
1670
|
+
font-size: var(--grim-text-xs);
|
|
1671
|
+
line-height: 1.3;
|
|
1113
1672
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1114
1673
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1674
|
+
/* The rating is one control with two halves — the count, which opens
|
|
1675
|
+
the forge thread, and the vote, which casts one — joined the way
|
|
1676
|
+
the sort combo is. It was two badges with a dash between them,
|
|
1677
|
+
which read as punctuation rather than as a control. */
|
|
1678
|
+
.rating-group {
|
|
1679
|
+
display: inline-flex;
|
|
1680
|
+
align-items: stretch;
|
|
1681
|
+
/* Above the card's full-card overlay (`.card h2 a::after`), so each
|
|
1682
|
+
half takes its own click instead of opening the package page.
|
|
1683
|
+
Stated rather than left to tree order: the overlay and these are
|
|
1684
|
+
both positioned, and "later in the DOM wins" is the kind of tie
|
|
1685
|
+
a later markup edit breaks silently. */
|
|
1686
|
+
position: relative;
|
|
1687
|
+
z-index: 1;
|
|
1120
1688
|
}
|
|
1121
|
-
|
|
1689
|
+
/* Same treatment `.pill` gives the version and licence badges beside
|
|
1690
|
+
them, applied per half so the border can be shared. */
|
|
1691
|
+
.rating-count,
|
|
1692
|
+
.rating-vote {
|
|
1693
|
+
display: inline-flex;
|
|
1694
|
+
align-items: center;
|
|
1695
|
+
gap: var(--grim-space-1);
|
|
1122
1696
|
font-size: var(--grim-text-xs);
|
|
1123
1697
|
color: var(--grim-color-muted);
|
|
1124
1698
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1125
|
-
|
|
1126
|
-
/* Optical, as above — the smallest badge in the UI. */
|
|
1699
|
+
/* Optical, matching `.pill` — the smallest badge in the UI. */
|
|
1127
1700
|
padding: 0.03rem var(--grim-space-4);
|
|
1701
|
+
text-decoration: none;
|
|
1128
1702
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1703
|
+
/* Lifted individually as well as through the group: whichever of the
|
|
1704
|
+
two ends up establishing a stacking context, the link is above the
|
|
1705
|
+
card overlay and takes its own click. */
|
|
1706
|
+
.rating-count,
|
|
1707
|
+
.rating-vote {
|
|
1708
|
+
position: relative;
|
|
1709
|
+
z-index: 1;
|
|
1710
|
+
}
|
|
1711
|
+
.rating-count {
|
|
1712
|
+
border-radius: var(--grim-radius-pill) 0 0 var(--grim-radius-pill);
|
|
1713
|
+
/* Opaque, like its tinted other half. A bordered control with a
|
|
1714
|
+
see-through interior let the kind watermark run straight through
|
|
1715
|
+
the count — the one place on the card where a big glyph and a
|
|
1716
|
+
small control land on the same corner. */
|
|
1717
|
+
background: var(--grim-color-card);
|
|
1131
1718
|
}
|
|
1132
|
-
/*
|
|
1133
|
-
|
|
1134
|
-
.
|
|
1135
|
-
|
|
1136
|
-
align-items: center;
|
|
1137
|
-
gap: var(--grim-space-1);
|
|
1719
|
+
/* An index with no VS Code extension configured shows the count
|
|
1720
|
+
alone, and a lone half is a whole pill again. */
|
|
1721
|
+
.rating-count:last-child {
|
|
1722
|
+
border-radius: var(--grim-radius-pill);
|
|
1138
1723
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1724
|
+
/* Tinted rather than outlined differently: this is the half that
|
|
1725
|
+
posts something, and a shade says so without a second border
|
|
1726
|
+
colour competing with the hover state. Pulled onto its neighbour's
|
|
1727
|
+
border so the seam is one line, derived from the token. */
|
|
1728
|
+
.rating-vote {
|
|
1729
|
+
border-radius: 0 var(--grim-radius-pill) var(--grim-radius-pill) 0;
|
|
1730
|
+
margin-left: calc(-1 * var(--grim-border-width));
|
|
1731
|
+
background: var(--grim-color-chip-bg);
|
|
1732
|
+
/* Wider on the outside than the seam: the glyph sat against the
|
|
1733
|
+
rounded edge, which reads as clipped rather than as inset. */
|
|
1734
|
+
padding-inline: var(--grim-space-3) var(--grim-space-5);
|
|
1142
1735
|
}
|
|
1143
|
-
.
|
|
1736
|
+
a.rating-count:hover,
|
|
1737
|
+
a.rating-vote:hover {
|
|
1738
|
+
color: var(--grim-color-accent);
|
|
1739
|
+
border-color: var(--grim-color-accent);
|
|
1740
|
+
/* 2, not 1: both halves already sit at 1 so they clear the card
|
|
1741
|
+
overlay, and the vote half is pulled onto the count's right
|
|
1742
|
+
border by a negative margin. At equal z-index the later element
|
|
1743
|
+
wins, so hovering the count lit an edge its neighbour was
|
|
1744
|
+
covering — the accent appeared on three sides and stopped. */
|
|
1745
|
+
z-index: 2;
|
|
1746
|
+
}
|
|
1747
|
+
/* Version and recency as one aside. Italic and a step below the rest
|
|
1748
|
+
on purpose — it is a note about the card, not one of its facts, and
|
|
1749
|
+
it sits opposite two buttons that should keep the eye. Bottom-
|
|
1750
|
+
aligned rather than centred on them, since a centred aside reads as
|
|
1751
|
+
a third control in the row. */
|
|
1752
|
+
.card-meta {
|
|
1144
1753
|
margin: 0;
|
|
1145
|
-
|
|
1146
|
-
|
|
1754
|
+
align-self: flex-end;
|
|
1755
|
+
font-size: var(--grim-text-2xs);
|
|
1756
|
+
font-style: italic;
|
|
1757
|
+
color: var(--grim-color-muted);
|
|
1758
|
+
white-space: nowrap;
|
|
1759
|
+
}
|
|
1760
|
+
.card-version {
|
|
1761
|
+
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1762
|
+
font-style: normal;
|
|
1147
1763
|
}
|
|
1148
1764
|
/* Long descriptions would stretch their whole grid row; clamp to 3
|
|
1149
1765
|
lines so cards stay uniform. Full text lives on the detail page. */
|
|
1766
|
+
/* Exactly two lines, whether it needs them or not.
|
|
1767
|
+
`line-clamp` alone only caps the tall ones; a one-line description
|
|
1768
|
+
still drew a shorter block, so every card in a row started its
|
|
1769
|
+
keywords and its buttons at a different height. `min-height` is the
|
|
1770
|
+
other half — two lines of this leading, reserved — and the two
|
|
1771
|
+
together make the block a fixed size regardless of the text. */
|
|
1150
1772
|
.description {
|
|
1151
1773
|
margin: 0;
|
|
1152
1774
|
font-size: var(--grim-text-md);
|
|
1775
|
+
line-height: 1.45;
|
|
1776
|
+
min-height: calc(2 * 1.45em);
|
|
1153
1777
|
/* `auto` basis, not `0`: overflow:hidden drops the min-height:auto
|
|
1154
1778
|
floor, so a 0-basis item can collapse in an auto-height column. */
|
|
1155
|
-
flex:
|
|
1779
|
+
flex: 0 0 auto;
|
|
1156
1780
|
display: -webkit-box;
|
|
1157
1781
|
-webkit-box-orient: vertical;
|
|
1158
|
-
-webkit-line-clamp:
|
|
1159
|
-
line-clamp:
|
|
1782
|
+
-webkit-line-clamp: 2;
|
|
1783
|
+
line-clamp: 2;
|
|
1160
1784
|
overflow: hidden;
|
|
1161
1785
|
}
|
|
1786
|
+
/* One row, always. A card in an auto-fill grid has to keep its
|
|
1787
|
+
height predictable, and a package with long keywords was wrapping
|
|
1788
|
+
to a second line and pushing the foot down out of step with its
|
|
1789
|
+
neighbours.
|
|
1790
|
+
|
|
1791
|
+
Clipped rather than counted: a "+N" chip sits at the end of the
|
|
1792
|
+
row, so it is the first thing the clip eats, and it would be
|
|
1793
|
+
claiming a number while the row silently hid more. The fade says
|
|
1794
|
+
"there is more" without naming a figure, and the full list is on
|
|
1795
|
+
the page the card links to. Same treatment the command field uses
|
|
1796
|
+
for its overflow. */
|
|
1797
|
+
/* One row, whatever it holds: `nowrap` plus `hidden` means a set of
|
|
1798
|
+
keywords too wide for the card is cut off rather than wrapped onto
|
|
1799
|
+
a second row that would unbalance the grid.
|
|
1800
|
+
|
|
1801
|
+
The fade is on the row, which is full-width — so it always sits at
|
|
1802
|
+
the card's right edge, and on a row whose chips fit it falls
|
|
1803
|
+
entirely on empty space and shows nothing. Overflow is exactly the
|
|
1804
|
+
case where a chip reaches under it, which is why no measurement
|
|
1805
|
+
and no JavaScript is needed to decide. */
|
|
1162
1806
|
.keywords {
|
|
1163
1807
|
display: flex;
|
|
1164
|
-
flex-wrap:
|
|
1808
|
+
flex-wrap: nowrap;
|
|
1165
1809
|
gap: var(--grim-space-2);
|
|
1810
|
+
overflow: hidden;
|
|
1811
|
+
mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
|
|
1812
|
+
}
|
|
1813
|
+
/* Each chip keeps its own width; the row gives up, not the words. */
|
|
1814
|
+
.chip.keyword {
|
|
1815
|
+
flex: 0 0 auto;
|
|
1166
1816
|
}
|
|
1167
1817
|
.chip.keyword {
|
|
1168
1818
|
font-size: var(--grim-text-2xs);
|
|
1169
1819
|
padding: var(--grim-space-1) var(--grim-space-4);
|
|
1170
1820
|
color: var(--grim-color-muted);
|
|
1171
1821
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1822
|
+
/* A card chip applies the keyword facet, so it can be the one already
|
|
1823
|
+
applied — it says so rather than staying muted like the rest. */
|
|
1824
|
+
.chip.keyword.active {
|
|
1825
|
+
color: var(--grim-color-fg);
|
|
1174
1826
|
}
|
|
1175
1827
|
.card-foot {
|
|
1176
1828
|
display: flex;
|
|
1177
1829
|
align-items: center;
|
|
1178
1830
|
justify-content: space-between;
|
|
1179
1831
|
gap: var(--grim-space-4);
|
|
1180
|
-
|
|
1832
|
+
}
|
|
1833
|
+
/* The slack goes above the buttons, so they line up across a row
|
|
1834
|
+
however tall the cards are. The foot is the last group on every
|
|
1835
|
+
card — the keywords moved up under the head — so there is one
|
|
1836
|
+
element to hang it on rather than one per layout. */
|
|
1837
|
+
.card-foot {
|
|
1838
|
+
margin-top: auto;
|
|
1181
1839
|
}
|
|
1182
1840
|
.copy-group {
|
|
1183
1841
|
display: flex;
|
|
@@ -1192,7 +1850,7 @@ function repoLabel(url: string): string {
|
|
|
1192
1850
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1193
1851
|
background: var(--grim-color-chip-bg);
|
|
1194
1852
|
color: var(--grim-color-fg);
|
|
1195
|
-
border-radius: var(--grim-radius-
|
|
1853
|
+
border-radius: var(--grim-radius-inset);
|
|
1196
1854
|
padding: var(--grim-space-2) var(--grim-space-4);
|
|
1197
1855
|
font-size: var(--grim-text-sm);
|
|
1198
1856
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
@@ -1228,7 +1886,7 @@ function repoLabel(url: string): string {
|
|
|
1228
1886
|
margin: 0;
|
|
1229
1887
|
padding: var(--grim-space-6) var(--grim-space-6);
|
|
1230
1888
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1231
|
-
border-radius: var(--grim-radius-
|
|
1889
|
+
border-radius: var(--grim-radius-control);
|
|
1232
1890
|
overflow-x: auto;
|
|
1233
1891
|
font-size: var(--grim-text-sm);
|
|
1234
1892
|
line-height: 1.5;
|
|
@@ -1267,7 +1925,7 @@ function repoLabel(url: string): string {
|
|
|
1267
1925
|
height: 1.9rem;
|
|
1268
1926
|
padding: 0;
|
|
1269
1927
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1270
|
-
border-radius: var(--grim-radius-
|
|
1928
|
+
border-radius: var(--grim-radius-inset);
|
|
1271
1929
|
background: var(--grim-color-card);
|
|
1272
1930
|
color: var(--grim-color-muted);
|
|
1273
1931
|
cursor: pointer;
|
|
@@ -1305,10 +1963,6 @@ function repoLabel(url: string): string {
|
|
|
1305
1963
|
gap: 0;
|
|
1306
1964
|
text-decoration: none;
|
|
1307
1965
|
}
|
|
1308
|
-
.source {
|
|
1309
|
-
font-size: var(--grim-text-sm);
|
|
1310
|
-
white-space: nowrap;
|
|
1311
|
-
}
|
|
1312
1966
|
.empty {
|
|
1313
1967
|
color: var(--grim-color-muted);
|
|
1314
1968
|
}
|