@grimoire-rs/indexer 0.5.1 → 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 +37 -1
- package/dist/renderer/astro/components/Catalog.js +67 -51
- package/dist/renderer/astro/components/Catalog.tsx +74 -52
- 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 +53 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,41 @@ props `{ pkg }` → `{ pkg, compact }`.
|
|
|
21
21
|
|
|
22
22
|
## [Unreleased]
|
|
23
23
|
|
|
24
|
+
## [0.5.2] - 2026-08-31
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **The sort control drew two borders when clicked.** Hover tints a control's
|
|
29
|
+
border to the accent and focus drew a 2px accent outline standing 1px clear
|
|
30
|
+
of it, so anyone who reached the control with a mouse had both at once — one
|
|
31
|
+
state reading as two lines with a gap between them. The ring merges onto the
|
|
32
|
+
border now, the way the card's focus state already did, and the offset comes
|
|
33
|
+
off every bordered control in the toolbar together rather than off the one
|
|
34
|
+
that showed it: the search field, the keyword overflow menu's search, both
|
|
35
|
+
sort halves and both view picks. Keyword chips gain the same ring, having
|
|
36
|
+
taken the browser's default until now.
|
|
37
|
+
- **A long address no longer wraps the card's head onto a second line.** A
|
|
38
|
+
namespace breaks at its own slashes and dots, so a wide one took two rows and
|
|
39
|
+
left the card taller than its neighbours with the tail of the address against
|
|
40
|
+
the padding edge. It stays on one line and the overflow is trimmed from the
|
|
41
|
+
*front* — the registry host is on every address in an index and the
|
|
42
|
+
repository at the tail is what tells two apart — with the whole of it in the
|
|
43
|
+
element's `title`.
|
|
44
|
+
- **Keyword chips could stutter, or stop part-way through the rail's slide.**
|
|
45
|
+
The rail's FLIP pass inverted each chip with an inline `transition: none` and
|
|
46
|
+
a `translate`, then dropped both on the next animation frame; any commit
|
|
47
|
+
landing inside that window left the chip carrying the offset with its
|
|
48
|
+
transition disabled, and nothing else took those off. It also measured seats
|
|
49
|
+
with `getBoundingClientRect`, which reports where a chip is *drawn*, so a
|
|
50
|
+
chip caught mid-slide recorded its animated box and the next inversion
|
|
51
|
+
compounded the error instead of correcting it. Neither window was rare: the
|
|
52
|
+
rail's own fit measurement re-commits whenever a rescore changes how many
|
|
53
|
+
chips fit. Seats now come from `offsetLeft`/`offsetTop`, which ignore
|
|
54
|
+
transforms and scroll, and the slide is a Web Animation — it starts without a
|
|
55
|
+
frame, writes nothing to `style`, and clears itself when it finishes or is
|
|
56
|
+
cancelled. `prefers-reduced-motion` is honoured by the effect rather than by
|
|
57
|
+
the stylesheet, and `--grim-duration-slow` is still its length.
|
|
58
|
+
|
|
24
59
|
## [0.5.1] - 2026-08-31
|
|
25
60
|
|
|
26
61
|
### Added
|
|
@@ -265,6 +300,7 @@ props `{ pkg }` → `{ pkg, compact }`.
|
|
|
265
300
|
boot can leave a `.index-*` holding Vite's `deps_temp_<hash>`, which the
|
|
266
301
|
dependency optimizer recreates after the removal has already returned.
|
|
267
302
|
|
|
268
|
-
[Unreleased]: https://github.com/grimoire-rs/indexer/compare/v0.5.
|
|
303
|
+
[Unreleased]: https://github.com/grimoire-rs/indexer/compare/v0.5.2...HEAD
|
|
304
|
+
[0.5.2]: https://github.com/grimoire-rs/indexer/compare/v0.5.1...v0.5.2
|
|
269
305
|
[0.5.1]: https://github.com/grimoire-rs/indexer/compare/v0.5.0...v0.5.1
|
|
270
306
|
[0.5.0]: https://github.com/grimoire-rs/indexer/compare/v0.4.4...v0.5.0
|
|
@@ -258,7 +258,18 @@ export default function Catalog({ packages, vscodeExtension, }) {
|
|
|
258
258
|
...(controlsRef.current?.querySelectorAll('button.chip:not([aria-hidden="true"])') ?? []),
|
|
259
259
|
];
|
|
260
260
|
const railRefs = useRef(new Map());
|
|
261
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Where each chip sat at the last commit, as `offsetLeft`/`offsetTop`.
|
|
263
|
+
*
|
|
264
|
+
* Layout positions, deliberately, not `getBoundingClientRect`: the offsets
|
|
265
|
+
* ignore transforms and page scroll, so a chip measured mid-slide reports
|
|
266
|
+
* the seat it is animating towards rather than the box it is drawn in this
|
|
267
|
+
* frame. See the FLIP effect below for why that distinction is the whole
|
|
268
|
+
* fix.
|
|
269
|
+
*/
|
|
270
|
+
const railSeats = useRef(new Map());
|
|
271
|
+
/** The slide each chip is currently running, so a new one can replace it. */
|
|
272
|
+
const railSlides = useRef(new WeakMap());
|
|
262
273
|
const railRef = useRef(null);
|
|
263
274
|
const kwMenuRef = useRef(null);
|
|
264
275
|
const kwTriggerRef = useRef(null);
|
|
@@ -373,71 +384,76 @@ export default function Catalog({ packages, vscodeExtension, }) {
|
|
|
373
384
|
* between two frames reads as having been *replaced*, and a reader who
|
|
374
385
|
* cannot see that a chip moved has no reason to believe it is the same one.
|
|
375
386
|
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
387
|
+
* Two choices carry the whole thing, and both are here because the shape
|
|
388
|
+
* they replace — an inline `translate` under the stylesheet's transition,
|
|
389
|
+
* dropped again on a `requestAnimationFrame` — could leave a chip stopped
|
|
390
|
+
* off its seat with no path back:
|
|
391
|
+
*
|
|
392
|
+
* - **Seats come from `offsetLeft`/`offsetTop`, never from
|
|
393
|
+
* `getBoundingClientRect`.** The offsets are layout positions and ignore
|
|
394
|
+
* both transforms and scroll; a rect is where the chip is *drawn*, so a
|
|
395
|
+
* chip measured mid-slide recorded its animated box and the next
|
|
396
|
+
* inversion compounded that error — the stutter. Mid-slide is the common
|
|
397
|
+
* case, not a rare one: the fit measurement above commits a second time
|
|
398
|
+
* whenever a rescore changes how many chips fit, and that commit lands
|
|
399
|
+
* inside the previous slide. Because the offsets are transform-blind,
|
|
400
|
+
* that second commit now measures the same seats and starts nothing.
|
|
401
|
+
* - **The slide is a Web Animation, not an inline style.** It needs no
|
|
402
|
+
* frame to start, so no commit can land between an invert and its play
|
|
403
|
+
* and freeze a chip at the offset. It writes nothing to `style`, so
|
|
404
|
+
* there is nothing left to strip when a chip unmounts or a pass is
|
|
405
|
+
* superseded. And it clears itself the instant it finishes or is
|
|
406
|
+
* cancelled, which makes "the rail always ends up in its real state" a
|
|
407
|
+
* property of the mechanism rather than of a cleanup remembering to run.
|
|
382
408
|
*/
|
|
383
409
|
useLayoutEffect(() => {
|
|
384
|
-
|
|
385
|
-
// anything. Two reasons, and both were visible: `getBoundingClientRect`
|
|
386
|
-
// reports the *translated* box, so a chip caught mid-slide would be
|
|
387
|
-
// measured where it is drawn rather than where it belongs and the next
|
|
388
|
-
// inversion would compound that error; and a chip whose play frame never
|
|
389
|
-
// ran is still carrying `transition: none` with an offset, which is a chip
|
|
390
|
-
// frozen off its seat. Clearing here is what unfreezes it.
|
|
391
|
-
//
|
|
392
|
-
// Mid-slide is not a rare case: the fit measurement above commits a second
|
|
393
|
-
// time whenever the rescore changes how many chips fit, and that commit
|
|
394
|
-
// lands between this one and its animation frame.
|
|
395
|
-
for (const el of railRefs.current.values()) {
|
|
396
|
-
el.style.transition = "";
|
|
397
|
-
el.style.translate = "";
|
|
398
|
-
}
|
|
399
|
-
const previous = railRects.current;
|
|
410
|
+
const previous = railSeats.current;
|
|
400
411
|
const current = new Map();
|
|
401
412
|
const moved = [];
|
|
402
|
-
// A second pass, deliberately: every write above is flushed before the
|
|
403
|
-
// first read below, rather than interleaving them per chip.
|
|
404
413
|
for (const [keyword, el] of railRefs.current) {
|
|
405
|
-
const
|
|
406
|
-
|
|
414
|
+
const x = el.offsetLeft;
|
|
415
|
+
const y = el.offsetTop;
|
|
416
|
+
current.set(keyword, { x, y });
|
|
407
417
|
const was = previous.get(keyword);
|
|
408
418
|
if (!was)
|
|
409
419
|
continue;
|
|
410
|
-
const dx = was.
|
|
411
|
-
const dy = was.
|
|
420
|
+
const dx = was.x - x;
|
|
421
|
+
const dy = was.y - y;
|
|
412
422
|
if (dx !== 0 || dy !== 0)
|
|
413
423
|
moved.push({ el, dx, dy });
|
|
414
424
|
}
|
|
415
|
-
|
|
425
|
+
// Rebuilt from `railRefs` every pass, so a chip that left the rail leaves
|
|
426
|
+
// this map with it and cannot seed a slide if it comes back elsewhere.
|
|
427
|
+
railSeats.current = current;
|
|
416
428
|
if (moved.length === 0)
|
|
417
429
|
return;
|
|
430
|
+
const rail = railRef.current;
|
|
431
|
+
// Guarded rather than assumed, like the fit observer above: the test
|
|
432
|
+
// renderer's DOM has no Web Animations API, and a rail that does not
|
|
433
|
+
// slide is not worth throwing during a render over.
|
|
434
|
+
if (!rail || typeof rail.animate !== "function")
|
|
435
|
+
return;
|
|
436
|
+
// The motion is the ornament here — the filtering works identically
|
|
437
|
+
// without it — so a reader who asked for less of it gets none.
|
|
438
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
439
|
+
return;
|
|
440
|
+
// The stylesheet stays the source of truth for how long a slide runs:
|
|
441
|
+
// `--grim-duration-slow` is written in milliseconds and that is the unit
|
|
442
|
+
// the Web Animations API takes, so the token survives the move out of CSS.
|
|
443
|
+
// Read once, after every seat above, so the reads and the writes below
|
|
444
|
+
// stay in two passes rather than interleaving per chip.
|
|
445
|
+
const ms = Number.parseFloat(getComputedStyle(rail).getPropertyValue("--grim-duration-slow"));
|
|
418
446
|
for (const { el, dx, dy } of moved) {
|
|
419
|
-
|
|
420
|
-
|
|
447
|
+
// One slide per chip. A chip that moves again mid-slide takes the new
|
|
448
|
+
// delta from its layout seat, which starts the second slide a step off
|
|
449
|
+
// where the first had drawn it — a jump measured in the pixels one
|
|
450
|
+
// frame of easing covers, and it is bounded, unlike two animations
|
|
451
|
+
// compositing the same property against each other.
|
|
452
|
+
// ponytail: not blended with the in-flight offset, which would cost a
|
|
453
|
+
// computed-style read per chip in the middle of the write pass.
|
|
454
|
+
railSlides.current.get(el)?.cancel();
|
|
455
|
+
railSlides.current.set(el, el.animate({ translate: [`${dx}px ${dy}px`, "none"] }, { duration: Number.isFinite(ms) ? ms : 200, easing: "ease-out" }));
|
|
421
456
|
}
|
|
422
|
-
const frame = requestAnimationFrame(() => {
|
|
423
|
-
for (const { el } of moved) {
|
|
424
|
-
el.style.transition = "";
|
|
425
|
-
el.style.translate = "";
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
return () => {
|
|
429
|
-
cancelAnimationFrame(frame);
|
|
430
|
-
// Cancelling is not enough on its own. Nothing else takes these off, so
|
|
431
|
-
// a commit landing before the frame ran would leave every moved chip
|
|
432
|
-
// sitting at its inverted offset with transitions disabled — the rail
|
|
433
|
-
// stopping halfway and staying there. Deselecting the last keyword is
|
|
434
|
-
// the reliable way to see it: the rescore is at its largest, so the fit
|
|
435
|
-
// changes and the extra commit always lands.
|
|
436
|
-
for (const { el } of moved) {
|
|
437
|
-
el.style.transition = "";
|
|
438
|
-
el.style.translate = "";
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
457
|
});
|
|
442
458
|
/** Move focus `delta` cards along, clamping at both ends rather than wrapping. */
|
|
443
459
|
const focusCard = (from, delta) => {
|
|
@@ -334,7 +334,18 @@ export default function Catalog({
|
|
|
334
334
|
];
|
|
335
335
|
|
|
336
336
|
const railRefs = useRef(new Map<string, HTMLElement>());
|
|
337
|
-
|
|
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>());
|
|
338
349
|
const railRef = useRef<HTMLDivElement>(null);
|
|
339
350
|
|
|
340
351
|
const kwMenuRef = useRef<HTMLDivElement>(null);
|
|
@@ -450,68 +461,79 @@ export default function Catalog({
|
|
|
450
461
|
* between two frames reads as having been *replaced*, and a reader who
|
|
451
462
|
* cannot see that a chip moved has no reason to believe it is the same one.
|
|
452
463
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
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.
|
|
459
485
|
*/
|
|
460
486
|
useLayoutEffect(() => {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// reports the *translated* box, so a chip caught mid-slide would be
|
|
464
|
-
// measured where it is drawn rather than where it belongs and the next
|
|
465
|
-
// inversion would compound that error; and a chip whose play frame never
|
|
466
|
-
// ran is still carrying `transition: none` with an offset, which is a chip
|
|
467
|
-
// frozen off its seat. Clearing here is what unfreezes it.
|
|
468
|
-
//
|
|
469
|
-
// Mid-slide is not a rare case: the fit measurement above commits a second
|
|
470
|
-
// time whenever the rescore changes how many chips fit, and that commit
|
|
471
|
-
// lands between this one and its animation frame.
|
|
472
|
-
for (const el of railRefs.current.values()) {
|
|
473
|
-
el.style.transition = "";
|
|
474
|
-
el.style.translate = "";
|
|
475
|
-
}
|
|
476
|
-
const previous = railRects.current;
|
|
477
|
-
const current = new Map<string, DOMRect>();
|
|
487
|
+
const previous = railSeats.current;
|
|
488
|
+
const current = new Map<string, { x: number; y: number }>();
|
|
478
489
|
const moved: { el: HTMLElement; dx: number; dy: number }[] = [];
|
|
479
|
-
// A second pass, deliberately: every write above is flushed before the
|
|
480
|
-
// first read below, rather than interleaving them per chip.
|
|
481
490
|
for (const [keyword, el] of railRefs.current) {
|
|
482
|
-
const
|
|
483
|
-
|
|
491
|
+
const x = el.offsetLeft;
|
|
492
|
+
const y = el.offsetTop;
|
|
493
|
+
current.set(keyword, { x, y });
|
|
484
494
|
const was = previous.get(keyword);
|
|
485
495
|
if (!was) continue;
|
|
486
|
-
const dx = was.
|
|
487
|
-
const dy = was.
|
|
496
|
+
const dx = was.x - x;
|
|
497
|
+
const dy = was.y - y;
|
|
488
498
|
if (dx !== 0 || dy !== 0) moved.push({ el, dx, dy });
|
|
489
499
|
}
|
|
490
|
-
|
|
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;
|
|
491
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
|
+
);
|
|
492
520
|
for (const { el, dx, dy } of moved) {
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
);
|
|
495
536
|
}
|
|
496
|
-
const frame = requestAnimationFrame(() => {
|
|
497
|
-
for (const { el } of moved) {
|
|
498
|
-
el.style.transition = "";
|
|
499
|
-
el.style.translate = "";
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
return () => {
|
|
503
|
-
cancelAnimationFrame(frame);
|
|
504
|
-
// Cancelling is not enough on its own. Nothing else takes these off, so
|
|
505
|
-
// a commit landing before the frame ran would leave every moved chip
|
|
506
|
-
// sitting at its inverted offset with transitions disabled — the rail
|
|
507
|
-
// stopping halfway and staying there. Deselecting the last keyword is
|
|
508
|
-
// the reliable way to see it: the rescore is at its largest, so the fit
|
|
509
|
-
// changes and the extra commit always lands.
|
|
510
|
-
for (const { el } of moved) {
|
|
511
|
-
el.style.transition = "";
|
|
512
|
-
el.style.translate = "";
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
537
|
});
|
|
516
538
|
|
|
517
539
|
/** Move focus `delta` cards along, clamping at both ends rather than wrapping. */
|
|
@@ -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
|
|
@@ -849,10 +849,6 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
849
849
|
font-size: var(--grim-text-md);
|
|
850
850
|
min-width: 0;
|
|
851
851
|
}
|
|
852
|
-
.controls input[type="search"]:focus-visible {
|
|
853
|
-
outline: 2px solid var(--grim-color-accent);
|
|
854
|
-
outline-offset: 1px;
|
|
855
|
-
}
|
|
856
852
|
/* The `/` shortcut, shown as the key it is. It is a hint for a field
|
|
857
853
|
that is not yet in use, so it withdraws the moment the field is
|
|
858
854
|
focused or holds a query — at which point it would only be
|
|
@@ -1026,11 +1022,6 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1026
1022
|
.sort-field:focus-visible {
|
|
1027
1023
|
z-index: 1;
|
|
1028
1024
|
}
|
|
1029
|
-
.sort-dir:focus-visible,
|
|
1030
|
-
.sort-field:focus-visible {
|
|
1031
|
-
outline: 2px solid var(--grim-color-accent);
|
|
1032
|
-
outline-offset: 1px;
|
|
1033
|
-
}
|
|
1034
1025
|
/* One box for every chip in the filter row — the kinds, the keywords,
|
|
1035
1026
|
the deprecated toggle and the overflow menu's trigger alike. The
|
|
1036
1027
|
line-height is stated rather than inherited because a `<summary>`
|
|
@@ -1062,21 +1053,13 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1062
1053
|
background: var(--grim-color-accent-soft);
|
|
1063
1054
|
}
|
|
1064
1055
|
/* The keyword rail reorders on every click — it is rescored against
|
|
1065
|
-
what is on screen — and the chips slide rather than jump.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
}
|
|
1056
|
+
what is on screen — and the chips slide rather than jump. There is
|
|
1057
|
+
deliberately no `translate` transition here: the FLIP effect in
|
|
1058
|
+
`Catalog.tsx` runs each slide as a Web Animation instead, reading
|
|
1059
|
+
`--grim-duration-slow` for its length and honouring
|
|
1060
|
+
`prefers-reduced-motion` itself. A transition needs an inline style
|
|
1061
|
+
to drive it, and an inline style is what a superseded pass used to
|
|
1062
|
+
leave behind on a chip stopped off its seat. */
|
|
1080
1063
|
/* The overflow menu. A popover, and it has to be one: `.filter-row`
|
|
1081
1064
|
above is a scroll container, and an absolutely-positioned panel
|
|
1082
1065
|
inside a scroll container is cropped to the row AND stretches the
|
|
@@ -1143,10 +1126,6 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1143
1126
|
font: inherit;
|
|
1144
1127
|
font-size: var(--grim-text-sm);
|
|
1145
1128
|
}
|
|
1146
|
-
.kw-menu-search:focus-visible {
|
|
1147
|
-
outline: 2px solid var(--grim-color-accent);
|
|
1148
|
-
outline-offset: 1px;
|
|
1149
|
-
}
|
|
1150
1129
|
.kw-menu-list {
|
|
1151
1130
|
display: flex;
|
|
1152
1131
|
flex-direction: column;
|
|
@@ -1240,9 +1219,26 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1240
1219
|
.view-pick.active {
|
|
1241
1220
|
z-index: 1;
|
|
1242
1221
|
}
|
|
1222
|
+
|
|
1223
|
+
/* One focus ring for every bordered control in the toolbar, and it is
|
|
1224
|
+
the card's: the border takes the accent and the outline sits
|
|
1225
|
+
directly on it, so the two merge into one thicker edge.
|
|
1226
|
+
|
|
1227
|
+
An offset ring is what put two borders on the sort combo. Hover
|
|
1228
|
+
already tints the border, so a ring standing 1px clear of it drew a
|
|
1229
|
+
second line in the same colour — one state reading as two. Every
|
|
1230
|
+
control here tints its border on hover or while active, so the
|
|
1231
|
+
offset had to go from all of them together rather than from the one
|
|
1232
|
+
that was reported. */
|
|
1233
|
+
.chip:focus-visible,
|
|
1234
|
+
.controls input[type="search"]:focus-visible,
|
|
1235
|
+
.kw-menu-search:focus-visible,
|
|
1236
|
+
.sort-dir:focus-visible,
|
|
1237
|
+
.sort-field:focus-visible,
|
|
1243
1238
|
.view-pick:focus-visible {
|
|
1244
1239
|
outline: 2px solid var(--grim-color-accent);
|
|
1245
|
-
outline-offset:
|
|
1240
|
+
outline-offset: 0;
|
|
1241
|
+
border-color: var(--grim-color-accent);
|
|
1246
1242
|
}
|
|
1247
1243
|
|
|
1248
1244
|
.grid {
|
|
@@ -1745,6 +1741,34 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1745
1741
|
font-size: var(--grim-text-xs);
|
|
1746
1742
|
line-height: 1.3;
|
|
1747
1743
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1744
|
+
/* One line, whatever the address is. A namespace breaks at its own
|
|
1745
|
+
slashes and dots, so left to itself a long one wrapped to a second
|
|
1746
|
+
line and made the card's head taller than its neighbours'. */
|
|
1747
|
+
display: flex;
|
|
1748
|
+
min-width: 0;
|
|
1749
|
+
white-space: nowrap;
|
|
1750
|
+
}
|
|
1751
|
+
/* Trimmed from the FRONT: `github.com/` is on every address in an
|
|
1752
|
+
index and the repository at the tail is what tells two apart, so the
|
|
1753
|
+
head is what to spend.
|
|
1754
|
+
|
|
1755
|
+
`direction: rtl` is the mechanism — `text-overflow` ellipsises at
|
|
1756
|
+
the line's inline END, which in an RTL box is its left edge, and
|
|
1757
|
+
`text-align: left` keeps a short address sitting under the name
|
|
1758
|
+
rather than flung to the right. The address itself still reads
|
|
1759
|
+
left-to-right: it is one run of Latin characters, so the bidi
|
|
1760
|
+
algorithm lays it out LTR inside the RTL box. That holds because a
|
|
1761
|
+
namespace neither starts nor ends with punctuation — one that did
|
|
1762
|
+
would have that character reordered to the far end.
|
|
1763
|
+
|
|
1764
|
+
The two-value `text-overflow: ellipsis ""` would say this directly
|
|
1765
|
+
and is Firefox-only. */
|
|
1766
|
+
.namespace .address {
|
|
1767
|
+
min-width: 0;
|
|
1768
|
+
overflow: hidden;
|
|
1769
|
+
text-overflow: ellipsis;
|
|
1770
|
+
direction: rtl;
|
|
1771
|
+
text-align: left;
|
|
1748
1772
|
}
|
|
1749
1773
|
/* The rating is one control with two halves — the count, which opens
|
|
1750
1774
|
the forge thread, and the vote, which casts one — joined the way
|