@grimoire-rs/indexer 0.5.0 → 0.5.2
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 +117 -1
- 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/main.d.ts.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/renderer/astro/components/Catalog.js +146 -24
- package/dist/renderer/astro/components/Catalog.tsx +200 -31
- package/dist/renderer/astro/components/CodeBlock.astro +74 -0
- package/dist/renderer/astro/components/PackageCard.js +1 -1
- package/dist/renderer/astro/components/PackageCard.tsx +9 -1
- package/dist/renderer/astro/layouts/Base.astro +196 -83
- 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 +9 -3
- package/dist/renderer/astro/lib/commands.js +10 -5
- package/dist/renderer/astro/lib/commands.ts +22 -6
- package/dist/renderer/astro/pages/p/[...slug].astro +23 -17
- package/dist/renderer/index.d.ts +19 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +18 -2
- package/dist/renderer/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ArrowUpNarrowWide,
|
|
13
13
|
LayoutGrid,
|
|
14
14
|
List,
|
|
15
|
+
X,
|
|
15
16
|
} from "lucide-preact";
|
|
16
17
|
import { PackageCard } from "./PackageCard.js";
|
|
17
18
|
import { PackageRow } from "./PackageRow.js";
|
|
@@ -41,6 +42,13 @@ export type View = "cards" | "table";
|
|
|
41
42
|
*/
|
|
42
43
|
const KEYWORD_CHIP_LIMIT = 8;
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `popovertarget` needs an id, and the catalog is a singleton on its page —
|
|
47
|
+
* one island, one toolbar, one overflow menu — so this is a constant rather
|
|
48
|
+
* than something generated per mount.
|
|
49
|
+
*/
|
|
50
|
+
const KEYWORD_MENU_ID = "grim-keyword-overflow";
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* The direction each field is *worth* reading first in — A→Z for a name,
|
|
46
54
|
* newest and best-liked first for the two ranked keys.
|
|
@@ -326,9 +334,71 @@ export default function Catalog({
|
|
|
326
334
|
];
|
|
327
335
|
|
|
328
336
|
const railRefs = useRef(new Map<string, HTMLElement>());
|
|
329
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Where each chip sat at the last commit, as `offsetLeft`/`offsetTop`.
|
|
339
|
+
*
|
|
340
|
+
* Layout positions, deliberately, not `getBoundingClientRect`: the offsets
|
|
341
|
+
* ignore transforms and page scroll, so a chip measured mid-slide reports
|
|
342
|
+
* the seat it is animating towards rather than the box it is drawn in this
|
|
343
|
+
* frame. See the FLIP effect below for why that distinction is the whole
|
|
344
|
+
* fix.
|
|
345
|
+
*/
|
|
346
|
+
const railSeats = useRef(new Map<string, { x: number; y: number }>());
|
|
347
|
+
/** The slide each chip is currently running, so a new one can replace it. */
|
|
348
|
+
const railSlides = useRef(new WeakMap<HTMLElement, Animation>());
|
|
330
349
|
const railRef = useRef<HTMLDivElement>(null);
|
|
331
350
|
|
|
351
|
+
const kwMenuRef = useRef<HTMLDivElement>(null);
|
|
352
|
+
const kwTriggerRef = useRef<HTMLButtonElement>(null);
|
|
353
|
+
// Drives the trigger's own `aria-expanded` and its open styling. The panel's
|
|
354
|
+
// visibility is the popover's business, not this flag's.
|
|
355
|
+
const [kwMenuOpen, setKwMenuOpen] = useState(false);
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Seat the overflow menu under its trigger.
|
|
359
|
+
*
|
|
360
|
+
* The panel is a popover, so it lives in the top layer and is positioned
|
|
361
|
+
* against the viewport rather than against any ancestor — which is the whole
|
|
362
|
+
* point (`.filter-row` is a scroll container and used to crop it). That
|
|
363
|
+
* leaves the seat to us.
|
|
364
|
+
*
|
|
365
|
+
* It always opens downward: the toolbar sits at the top of the page, and a
|
|
366
|
+
* flip would only ever fire on a viewport short enough that the panel has
|
|
367
|
+
* nowhere to go either way. What does not fit becomes `max-height` and
|
|
368
|
+
* scrolls inside the list.
|
|
369
|
+
*
|
|
370
|
+
* Called twice per open, from `beforetoggle` and again from `toggle`. The
|
|
371
|
+
* first runs while the panel is still `display: none`, so its measured width
|
|
372
|
+
* is 0 and the horizontal clamp is a no-op — but the vertical seat is right,
|
|
373
|
+
* which is what stops it appearing in the wrong place for a frame. The
|
|
374
|
+
* second has a real width and finishes the clamp.
|
|
375
|
+
*/
|
|
376
|
+
const placeKwMenu = () => {
|
|
377
|
+
const panel = kwMenuRef.current;
|
|
378
|
+
const trigger = kwTriggerRef.current;
|
|
379
|
+
if (!panel || !trigger) return;
|
|
380
|
+
const seat = trigger.getBoundingClientRect();
|
|
381
|
+
const gap = 8;
|
|
382
|
+
const width = panel.getBoundingClientRect().width;
|
|
383
|
+
panel.style.left = `${Math.max(gap, Math.min(seat.left, window.innerWidth - width - gap))}px`;
|
|
384
|
+
panel.style.top = `${seat.bottom + gap}px`;
|
|
385
|
+
panel.style.maxHeight = `${Math.max(120, window.innerHeight - seat.bottom - gap * 3)}px`;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// A fixed panel does not travel with the trigger, so it is re-seated rather
|
|
389
|
+
// than left behind. Only while open — there is nothing to follow otherwise.
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!kwMenuOpen) return;
|
|
392
|
+
const reseat = () => placeKwMenu();
|
|
393
|
+
// Capturing: the scroll may be any ancestor's, including `.filter-row`'s.
|
|
394
|
+
window.addEventListener("scroll", reseat, { capture: true, passive: true });
|
|
395
|
+
window.addEventListener("resize", reseat);
|
|
396
|
+
return () => {
|
|
397
|
+
window.removeEventListener("scroll", reseat, { capture: true });
|
|
398
|
+
window.removeEventListener("resize", reseat);
|
|
399
|
+
};
|
|
400
|
+
}, [kwMenuOpen]);
|
|
401
|
+
|
|
332
402
|
/**
|
|
333
403
|
* How many keyword chips actually fit on the row, measured.
|
|
334
404
|
*
|
|
@@ -391,39 +461,79 @@ export default function Catalog({
|
|
|
391
461
|
* between two frames reads as having been *replaced*, and a reader who
|
|
392
462
|
* cannot see that a chip moved has no reason to believe it is the same one.
|
|
393
463
|
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
464
|
+
* Two choices carry the whole thing, and both are here because the shape
|
|
465
|
+
* they replace — an inline `translate` under the stylesheet's transition,
|
|
466
|
+
* dropped again on a `requestAnimationFrame` — could leave a chip stopped
|
|
467
|
+
* off its seat with no path back:
|
|
468
|
+
*
|
|
469
|
+
* - **Seats come from `offsetLeft`/`offsetTop`, never from
|
|
470
|
+
* `getBoundingClientRect`.** The offsets are layout positions and ignore
|
|
471
|
+
* both transforms and scroll; a rect is where the chip is *drawn*, so a
|
|
472
|
+
* chip measured mid-slide recorded its animated box and the next
|
|
473
|
+
* inversion compounded that error — the stutter. Mid-slide is the common
|
|
474
|
+
* case, not a rare one: the fit measurement above commits a second time
|
|
475
|
+
* whenever a rescore changes how many chips fit, and that commit lands
|
|
476
|
+
* inside the previous slide. Because the offsets are transform-blind,
|
|
477
|
+
* that second commit now measures the same seats and starts nothing.
|
|
478
|
+
* - **The slide is a Web Animation, not an inline style.** It needs no
|
|
479
|
+
* frame to start, so no commit can land between an invert and its play
|
|
480
|
+
* and freeze a chip at the offset. It writes nothing to `style`, so
|
|
481
|
+
* there is nothing left to strip when a chip unmounts or a pass is
|
|
482
|
+
* superseded. And it clears itself the instant it finishes or is
|
|
483
|
+
* cancelled, which makes "the rail always ends up in its real state" a
|
|
484
|
+
* property of the mechanism rather than of a cleanup remembering to run.
|
|
400
485
|
*/
|
|
401
486
|
useLayoutEffect(() => {
|
|
402
|
-
const previous =
|
|
403
|
-
const current = new Map<string,
|
|
487
|
+
const previous = railSeats.current;
|
|
488
|
+
const current = new Map<string, { x: number; y: number }>();
|
|
404
489
|
const moved: { el: HTMLElement; dx: number; dy: number }[] = [];
|
|
405
490
|
for (const [keyword, el] of railRefs.current) {
|
|
406
|
-
const
|
|
407
|
-
|
|
491
|
+
const x = el.offsetLeft;
|
|
492
|
+
const y = el.offsetTop;
|
|
493
|
+
current.set(keyword, { x, y });
|
|
408
494
|
const was = previous.get(keyword);
|
|
409
495
|
if (!was) continue;
|
|
410
|
-
const dx = was.
|
|
411
|
-
const dy = was.
|
|
496
|
+
const dx = was.x - x;
|
|
497
|
+
const dy = was.y - y;
|
|
412
498
|
if (dx !== 0 || dy !== 0) moved.push({ el, dx, dy });
|
|
413
499
|
}
|
|
414
|
-
|
|
500
|
+
// Rebuilt from `railRefs` every pass, so a chip that left the rail leaves
|
|
501
|
+
// this map with it and cannot seed a slide if it comes back elsewhere.
|
|
502
|
+
railSeats.current = current;
|
|
415
503
|
if (moved.length === 0) return;
|
|
504
|
+
const rail = railRef.current;
|
|
505
|
+
// Guarded rather than assumed, like the fit observer above: the test
|
|
506
|
+
// renderer's DOM has no Web Animations API, and a rail that does not
|
|
507
|
+
// slide is not worth throwing during a render over.
|
|
508
|
+
if (!rail || typeof rail.animate !== "function") return;
|
|
509
|
+
// The motion is the ornament here — the filtering works identically
|
|
510
|
+
// without it — so a reader who asked for less of it gets none.
|
|
511
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
512
|
+
// The stylesheet stays the source of truth for how long a slide runs:
|
|
513
|
+
// `--grim-duration-slow` is written in milliseconds and that is the unit
|
|
514
|
+
// the Web Animations API takes, so the token survives the move out of CSS.
|
|
515
|
+
// Read once, after every seat above, so the reads and the writes below
|
|
516
|
+
// stay in two passes rather than interleaving per chip.
|
|
517
|
+
const ms = Number.parseFloat(
|
|
518
|
+
getComputedStyle(rail).getPropertyValue("--grim-duration-slow"),
|
|
519
|
+
);
|
|
416
520
|
for (const { el, dx, dy } of moved) {
|
|
417
|
-
|
|
418
|
-
|
|
521
|
+
// One slide per chip. A chip that moves again mid-slide takes the new
|
|
522
|
+
// delta from its layout seat, which starts the second slide a step off
|
|
523
|
+
// where the first had drawn it — a jump measured in the pixels one
|
|
524
|
+
// frame of easing covers, and it is bounded, unlike two animations
|
|
525
|
+
// compositing the same property against each other.
|
|
526
|
+
// ponytail: not blended with the in-flight offset, which would cost a
|
|
527
|
+
// computed-style read per chip in the middle of the write pass.
|
|
528
|
+
railSlides.current.get(el)?.cancel();
|
|
529
|
+
railSlides.current.set(
|
|
530
|
+
el,
|
|
531
|
+
el.animate(
|
|
532
|
+
{ translate: [`${dx}px ${dy}px`, "none"] },
|
|
533
|
+
{ duration: Number.isFinite(ms) ? ms : 200, easing: "ease-out" },
|
|
534
|
+
),
|
|
535
|
+
);
|
|
419
536
|
}
|
|
420
|
-
const frame = requestAnimationFrame(() => {
|
|
421
|
-
for (const { el } of moved) {
|
|
422
|
-
el.style.transition = "";
|
|
423
|
-
el.style.translate = "";
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
return () => cancelAnimationFrame(frame);
|
|
427
537
|
});
|
|
428
538
|
|
|
429
539
|
/** Move focus `delta` cards along, clamping at both ends rather than wrapping. */
|
|
@@ -961,14 +1071,42 @@ export default function Catalog({
|
|
|
961
1071
|
// catalog's keyword vocabulary is open-ended, and a few hundred
|
|
962
1072
|
// chips at once is a wall, not a control.
|
|
963
1073
|
//
|
|
964
|
-
// A
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
|
|
968
|
-
|
|
1074
|
+
// A popover, and not by preference: the panel used to be an
|
|
1075
|
+
// absolutely-positioned child of `.filter-row`, which is a scroll
|
|
1076
|
+
// container, so it was cropped to the row and stretched the row's
|
|
1077
|
+
// scroll extent — an invisible menu and two stray scrollbars. The
|
|
1078
|
+
// top layer is outside every ancestor's `overflow`. Escape and
|
|
1079
|
+
// light dismiss come with it; only the seat is ours to compute,
|
|
1080
|
+
// and that is `placeKwMenu` above.
|
|
1081
|
+
<div class="kw-menu">
|
|
1082
|
+
<button
|
|
1083
|
+
type="button"
|
|
1084
|
+
class="chip"
|
|
1085
|
+
data-slot="filter-chip"
|
|
1086
|
+
ref={kwTriggerRef}
|
|
1087
|
+
popovertarget={KEYWORD_MENU_ID}
|
|
1088
|
+
aria-expanded={kwMenuOpen}
|
|
1089
|
+
>
|
|
969
1090
|
+{menuKeywords.length} more
|
|
970
|
-
</
|
|
971
|
-
<div
|
|
1091
|
+
</button>
|
|
1092
|
+
<div
|
|
1093
|
+
class="kw-menu-panel"
|
|
1094
|
+
id={KEYWORD_MENU_ID}
|
|
1095
|
+
popover="auto"
|
|
1096
|
+
ref={kwMenuRef}
|
|
1097
|
+
// Both, and in this order: `beforetoggle` runs synchronously
|
|
1098
|
+
// inside the show steps, so the panel is seated before it is
|
|
1099
|
+
// ever painted; `toggle` runs after, when its width can
|
|
1100
|
+
// actually be measured for the clamp.
|
|
1101
|
+
onBeforeToggle={(e) => {
|
|
1102
|
+
setKwMenuOpen(e.newState === "open");
|
|
1103
|
+
placeKwMenu();
|
|
1104
|
+
}}
|
|
1105
|
+
onToggle={(e) => {
|
|
1106
|
+
setKwMenuOpen(e.newState === "open");
|
|
1107
|
+
placeKwMenu();
|
|
1108
|
+
}}
|
|
1109
|
+
>
|
|
972
1110
|
<input
|
|
973
1111
|
type="text"
|
|
974
1112
|
class="kw-menu-search"
|
|
@@ -996,7 +1134,38 @@ export default function Catalog({
|
|
|
996
1134
|
)}
|
|
997
1135
|
</div>
|
|
998
1136
|
</div>
|
|
999
|
-
</
|
|
1137
|
+
</div>
|
|
1138
|
+
)}
|
|
1139
|
+
{keywords.length > 0 && (
|
|
1140
|
+
// Lifts every keyword at once. Only rendered while there is
|
|
1141
|
+
// something to lift — a permanently visible control that does
|
|
1142
|
+
// nothing most of the time is a chip's width spent on nothing, and
|
|
1143
|
+
// this row is already the one that runs out of room first.
|
|
1144
|
+
//
|
|
1145
|
+
// Keywords only, deliberately. Escape clears the search and the
|
|
1146
|
+
// kinds along with them, and a button that quietly did the same
|
|
1147
|
+
// would undo a filter the reader did not ask about; the label
|
|
1148
|
+
// names exactly what it lifts.
|
|
1149
|
+
<button
|
|
1150
|
+
type="button"
|
|
1151
|
+
class="chip kw-clear"
|
|
1152
|
+
data-slot="filter-chip"
|
|
1153
|
+
title="Clear the keyword filters"
|
|
1154
|
+
onKeyDown={onChipKeyDown}
|
|
1155
|
+
onClick={(e) => {
|
|
1156
|
+
setKeywords([]);
|
|
1157
|
+
// This button is the last thing standing when it is pressed:
|
|
1158
|
+
// clearing the facets unmounts it, and focus would land on
|
|
1159
|
+
// `<body>`, sending a keyboard reader back to the top of the
|
|
1160
|
+
// document. `detail === 0` is a click synthesized by Enter or
|
|
1161
|
+
// Space, so a pointer user is left alone and a keyboard one
|
|
1162
|
+
// gets the toolbar's own anchor instead of nothing.
|
|
1163
|
+
if (e.detail === 0) searchRef.current?.focus();
|
|
1164
|
+
}}
|
|
1165
|
+
>
|
|
1166
|
+
<X size={13} aria-hidden="true" />
|
|
1167
|
+
clear {keywords.length}
|
|
1168
|
+
</button>
|
|
1000
1169
|
)}
|
|
1001
1170
|
{hasDeprecated && (
|
|
1002
1171
|
// A toggle, not a filter: `aria-pressed` rather than the `active`
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright 2026 The Grimoire Authors
|
|
4
|
+
|
|
5
|
+
// A highlighted code block wearing the site's own copy affordance, and
|
|
6
|
+
// optionally a link out to VS Code.
|
|
7
|
+
//
|
|
8
|
+
// It exists for the same reason `CommandBar.astro` does: an index's own page
|
|
9
|
+
// under `theme/pages/` should be able to draw what the rest of the site draws
|
|
10
|
+
// rather than a `<pre>` that looks almost like it. A setup guide showing a
|
|
11
|
+
// config snippet, a publish guide showing a workflow — both want this, and
|
|
12
|
+
// neither wants to restate the Shiki theme pair or the copy markup.
|
|
13
|
+
//
|
|
14
|
+
// ---
|
|
15
|
+
// import CodeBlock from "@grim/components/CodeBlock.astro";
|
|
16
|
+
// import { vscodeUrl } from "@grim/lib/catalog";
|
|
17
|
+
// import { data } from "@grim/lib/data";
|
|
18
|
+
// ---
|
|
19
|
+
// <CodeBlock code="grim add acme/code-review" name="add command"
|
|
20
|
+
// vscodeHref={vscodeUrl(data.config.vscodeExtension, "acme/code-review")}
|
|
21
|
+
// vscodeLabel="Open in VS Code" />
|
|
22
|
+
//
|
|
23
|
+
// The copy button itself is NOT rendered here. `Base.astro` injects one into
|
|
24
|
+
// every code block on the page — it has to, because a block inside rendered
|
|
25
|
+
// markdown has no markup to hang one on — and it fills the `.code-actions`
|
|
26
|
+
// slot below when it finds it. One button, one clipboard path, one toast, and
|
|
27
|
+
// no second copy of the markup to drift.
|
|
28
|
+
import { Code } from "astro:components";
|
|
29
|
+
import { mdiMicrosoftVisualStudioCode } from "@mdi/js";
|
|
30
|
+
import { BrandMark } from "./BrandMark.tsx";
|
|
31
|
+
import { SHIKI_THEMES } from "../lib/code";
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
/** The snippet, verbatim. What the copy button copies. */
|
|
35
|
+
code: string;
|
|
36
|
+
/** Shiki language id. `sh` covers the commands this site mostly shows. */
|
|
37
|
+
lang?: string;
|
|
38
|
+
/** What the copy toast calls this, e.g. `"add command"`. */
|
|
39
|
+
name?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Any URL for the trailing button — a `vscode://` deep link from
|
|
42
|
+
* `@grim/lib/catalog`'s builders, or one written by hand. Null or omitted
|
|
43
|
+
* draws no button, which is what an index with `vscodeExtension: null` gets
|
|
44
|
+
* from those builders.
|
|
45
|
+
*/
|
|
46
|
+
vscodeHref?: string | null;
|
|
47
|
+
/** Accessible name for that button. */
|
|
48
|
+
vscodeLabel?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
code,
|
|
53
|
+
lang = "sh",
|
|
54
|
+
name,
|
|
55
|
+
vscodeHref = null,
|
|
56
|
+
vscodeLabel = "Open in VS Code",
|
|
57
|
+
} = Astro.props;
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<div class="code-block" data-slot="code-block" data-copy-name={name}>
|
|
61
|
+
<Code code={code} lang={lang} themes={SHIKI_THEMES} />
|
|
62
|
+
{/* Present even when empty: it is where `Base.astro` puts the copy button,
|
|
63
|
+
and a block that renders one before the script runs would flash a second
|
|
64
|
+
one after it. */}
|
|
65
|
+
<span class="code-actions">
|
|
66
|
+
{
|
|
67
|
+
vscodeHref && (
|
|
68
|
+
<a class="code-vscode" href={vscodeHref} title={vscodeLabel} aria-label={vscodeLabel}>
|
|
69
|
+
<BrandMark path={mdiMicrosoftVisualStudioCode} size={15} />
|
|
70
|
+
</a>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
@@ -38,7 +38,7 @@ export function PackageCard({ pkg: p, vscodeExtension, activeKeywords, onToggleK
|
|
|
38
38
|
const thread = externalUrl(p.rating.url);
|
|
39
39
|
const vote = vscodeVoteUrl(vscodeExtension, p.ref);
|
|
40
40
|
return (_jsxs("span", { class: "rating-group", children: [thread ? (_jsxs("a", { class: "rating-count", href: thread, target: "_blank", rel: "noopener noreferrer", tabIndex: -1, title: `${votes} — open the thread to vote`, "aria-label": `${p.name}: ${votes}. Open the voting thread`, children: [_jsx(ArrowBigUp, { size: 13, "aria-hidden": "true" }), p.rating.up] })) : (_jsxs("span", { class: "rating-count", title: votes, children: [_jsx(ArrowBigUp, { size: 13, "aria-hidden": "true" }), p.rating.up] })), vote && (_jsx("a", { class: "rating-vote", href: vote, tabIndex: -1, title: "Upvote in VS Code", "aria-label": `Upvote ${p.name} in VS Code`, children: _jsx(BrandMark, { path: mdiMicrosoftVisualStudioCode, size: 12 }) }))] }));
|
|
41
|
-
})(), _jsxs("p", { class: "namespace", children: [_jsx("span", { class: "kind", "data-slot": "package-kind", children: p.kind }), _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), p.namespace] })] }), p.keywords && p.keywords.length > 0 && (_jsx("div", { class: "keywords", "data-slot": "package-keywords", children: p.keywords.slice(0, 5).map((kw) => (_jsx("button", { type: "button", class: activeKeywords.includes(kw)
|
|
41
|
+
})(), _jsxs("p", { class: "namespace", children: [_jsx("span", { class: "kind", "data-slot": "package-kind", children: p.kind }), _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), _jsx("span", { class: "address", title: p.namespace, children: p.namespace })] })] }), p.keywords && p.keywords.length > 0 && (_jsx("div", { class: "keywords", "data-slot": "package-keywords", children: p.keywords.slice(0, 5).map((kw) => (_jsx("button", { type: "button", class: activeKeywords.includes(kw)
|
|
42
42
|
? "chip keyword active"
|
|
43
43
|
: "chip keyword", "aria-pressed": activeKeywords.includes(kw), tabIndex: -1, onClick: () => onToggleKeyword(kw), children: kw }, kw))) })), p.description && _jsx("p", { class: "description", children: p.description }), _jsxs("div", { class: "card-foot", children: [_jsxs("div", { class: "copy-group", children: [_jsx(CopyButton, { command: `grim add --global ${p.ref}`, variant: "global", name: `global add for ${p.name}` }), _jsx(CopyButton, { command: `grim add ${p.ref}`, name: `project add for ${p.name}` }), vscodeUrl(vscodeExtension, p.ref) && (_jsx("a", { class: "copy vscode", href: vscodeUrl(vscodeExtension, p.ref), title: "Open in VS Code", "aria-label": `Open ${p.name} in VS Code`, tabIndex: -1, children: _jsx(BrandMark, { path: mdiMicrosoftVisualStudioCode }) }))] }), (() => {
|
|
44
44
|
const at = lastUpdated(p);
|
|
@@ -175,7 +175,15 @@ export function PackageCard({
|
|
|
175
175
|
{p.kind}
|
|
176
176
|
</span>
|
|
177
177
|
<span aria-hidden="true"> · </span>
|
|
178
|
-
{
|
|
178
|
+
{/* Its own element so the row can give the address the slack and
|
|
179
|
+
nothing else: the kind is one short word and keeps its width,
|
|
180
|
+
and what does not fit is dropped off the FRONT — a registry
|
|
181
|
+
host is the least distinguishing part of an address and the
|
|
182
|
+
repository is the most. `title` keeps the whole of it
|
|
183
|
+
reachable, since the ellipsis hides the head. */}
|
|
184
|
+
<span class="address" title={p.namespace}>
|
|
185
|
+
{p.namespace}
|
|
186
|
+
</span>
|
|
179
187
|
</p>
|
|
180
188
|
</div>
|
|
181
189
|
{/* Under the head, above the description: what the package
|