@grimoire-rs/indexer 0.5.1 → 0.5.3
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 +127 -1
- package/dist/renderer/astro/components/CardLogo.d.ts +2 -2
- package/dist/renderer/astro/components/CardLogo.tsx +2 -2
- package/dist/renderer/astro/components/Catalog.d.ts +12 -4
- package/dist/renderer/astro/components/Catalog.js +356 -88
- package/dist/renderer/astro/components/Catalog.tsx +442 -108
- package/dist/renderer/astro/components/PackageCard.d.ts +2 -2
- package/dist/renderer/astro/components/PackageCard.js +1 -1
- package/dist/renderer/astro/components/PackageCard.tsx +11 -3
- package/dist/renderer/astro/components/PackageRow.d.ts +2 -2
- package/dist/renderer/astro/components/PackageRow.tsx +2 -2
- package/dist/renderer/astro/layouts/Base.astro +145 -51
- package/dist/renderer/astro/lib/catalog.d.ts +31 -1
- package/dist/renderer/astro/lib/catalog.js +32 -0
- package/dist/renderer/astro/lib/catalog.ts +73 -1
- package/dist/renderer/astro/lib/search.d.ts +17 -0
- package/dist/renderer/astro/lib/search.js +138 -0
- package/dist/renderer/astro/lib/search.ts +157 -0
- package/dist/renderer/astro/pages/index.astro +45 -2
- package/dist/renderer/astro/styles/tokens.css +6 -1
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +52 -0
- package/dist/renderer/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CardPackage } from "../lib/catalog.js";
|
|
2
2
|
export interface PackageCardProps {
|
|
3
|
-
pkg:
|
|
3
|
+
pkg: CardPackage;
|
|
4
4
|
/**
|
|
5
5
|
* `publisher.extension` id behind the deep links, or `null`. A prop rather
|
|
6
6
|
* than a `lib/data` read: this island hydrates in the browser, and importing
|
|
@@ -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);
|
|
@@ -22,11 +22,11 @@ import {
|
|
|
22
22
|
timeAgo,
|
|
23
23
|
vscodeUrl,
|
|
24
24
|
vscodeVoteUrl,
|
|
25
|
-
type
|
|
25
|
+
type CardPackage,
|
|
26
26
|
} from "../lib/catalog.js";
|
|
27
27
|
|
|
28
28
|
export interface PackageCardProps {
|
|
29
|
-
pkg:
|
|
29
|
+
pkg: CardPackage;
|
|
30
30
|
/**
|
|
31
31
|
* `publisher.extension` id behind the deep links, or `null`. A prop rather
|
|
32
32
|
* than a `lib/data` read: this island hydrates in the browser, and importing
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CardPackage } from "../lib/catalog.js";
|
|
2
2
|
export interface PackageRowProps {
|
|
3
|
-
pkg:
|
|
3
|
+
pkg: CardPackage;
|
|
4
4
|
/** Whether this index publishes ratings at all — decided once, not per row. */
|
|
5
5
|
hasRatings: boolean;
|
|
6
6
|
/** The catalog's own arrow-key navigation. */
|
|
@@ -13,10 +13,10 @@ import { ArrowBigUp } from "lucide-preact";
|
|
|
13
13
|
import { CardLogo } from "./CardLogo.js";
|
|
14
14
|
import { DEPRECATED_MARK, KIND_MARKS, KindMark } from "./KindMark.js";
|
|
15
15
|
import { withBase } from "../lib/base.js";
|
|
16
|
-
import { lastUpdated, timeAgo, type
|
|
16
|
+
import { lastUpdated, timeAgo, type CardPackage } from "../lib/catalog.js";
|
|
17
17
|
|
|
18
18
|
export interface PackageRowProps {
|
|
19
|
-
pkg:
|
|
19
|
+
pkg: CardPackage;
|
|
20
20
|
/** Whether this index publishes ratings at all — decided once, not per row. */
|
|
21
21
|
hasRatings: boolean;
|
|
22
22
|
/** The catalog's own arrow-key navigation. */
|
|
@@ -62,16 +62,18 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
62
62
|
|
|
63
63
|
// Also before first paint: a deep link (a keyword chip on a package
|
|
64
64
|
// page) or a stored preference means the catalog the server rendered —
|
|
65
|
-
// every package, empty search box, name order, deprecated hidden
|
|
66
|
-
// the wrong one. Flag it so CSS holds the catalog back
|
|
67
|
-
// showing the full list and rearranging it a moment
|
|
68
|
-
// drops the flag once it has applied the view —
|
|
69
|
-
// after hydration, never the hydrating one
|
|
70
|
-
// failsafe for an island that never
|
|
71
|
-
// blank page.
|
|
65
|
+
// every package, empty search box, name order, deprecated hidden, as
|
|
66
|
+
// cards — is the wrong one. Flag it so CSS holds the catalog back
|
|
67
|
+
// rather than showing the full list and rearranging it a moment
|
|
68
|
+
// later. The island drops the flag once it has applied the view —
|
|
69
|
+
// which is the render after hydration, never the hydrating one
|
|
70
|
+
// itself; the timeout is the failsafe for an island that never
|
|
71
|
+
// hydrates, where a flash beats a blank page.
|
|
72
72
|
const view = new URLSearchParams(location.search);
|
|
73
|
-
const arranged = ["sort", "dir", "deprecated"].some((k) =>
|
|
74
|
-
|
|
73
|
+
const arranged = ["sort", "dir", "deprecated", "view"].some((k) =>
|
|
74
|
+
pref("grim.catalog." + k),
|
|
75
|
+
);
|
|
76
|
+
if (view.get("q") || view.get("kind") || view.get("kw") || arranged) {
|
|
75
77
|
document.documentElement.dataset.query = "";
|
|
76
78
|
setTimeout(() => delete document.documentElement.dataset.query, 3000);
|
|
77
79
|
}
|
|
@@ -849,20 +851,16 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
849
851
|
font-size: var(--grim-text-md);
|
|
850
852
|
min-width: 0;
|
|
851
853
|
}
|
|
852
|
-
.controls input[type="search"]:focus-visible {
|
|
853
|
-
outline: 2px solid var(--grim-color-accent);
|
|
854
|
-
outline-offset: 1px;
|
|
855
|
-
}
|
|
856
854
|
/* The `/` shortcut, shown as the key it is. It is a hint for a field
|
|
857
855
|
that is not yet in use, so it withdraws the moment the field is
|
|
858
856
|
focused or holds a query — at which point it would only be
|
|
859
857
|
sitting on top of what is being typed. */
|
|
860
|
-
.search-hint
|
|
858
|
+
.search-hint,
|
|
859
|
+
.search-clear {
|
|
861
860
|
position: absolute;
|
|
862
861
|
right: 0.5rem;
|
|
863
862
|
top: 50%;
|
|
864
863
|
translate: 0 -50%;
|
|
865
|
-
pointer-events: none;
|
|
866
864
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
867
865
|
border-bottom-width: 2px;
|
|
868
866
|
border-radius: var(--grim-radius-inset);
|
|
@@ -873,10 +871,33 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
873
871
|
line-height: 1;
|
|
874
872
|
padding: var(--grim-space-2) var(--grim-space-3);
|
|
875
873
|
}
|
|
874
|
+
.search-hint {
|
|
875
|
+
pointer-events: none;
|
|
876
|
+
}
|
|
877
|
+
/* The two never coexist — the hint withdraws exactly when a query
|
|
878
|
+
appears, which is when the clear button is rendered — so they share
|
|
879
|
+
one corner rather than sitting beside each other. */
|
|
876
880
|
.search-field:focus-within .search-hint,
|
|
877
881
|
.search-field:has(input:not(:placeholder-shown)) .search-hint {
|
|
878
882
|
display: none;
|
|
879
883
|
}
|
|
884
|
+
/* Sized to the glyph rather than to a text line, so the box matches
|
|
885
|
+
the hint's height instead of growing around an SVG on a baseline. */
|
|
886
|
+
.search-clear {
|
|
887
|
+
display: grid;
|
|
888
|
+
place-items: center;
|
|
889
|
+
cursor: pointer;
|
|
890
|
+
}
|
|
891
|
+
.search-clear:hover {
|
|
892
|
+
color: var(--grim-color-fg);
|
|
893
|
+
}
|
|
894
|
+
/* The UA's own clear button, gone: it renders in this same corner in
|
|
895
|
+
Chromium and Safari, in a style that answers to no token here, and
|
|
896
|
+
it cannot be restyled beyond hiding. Ours replaces it. */
|
|
897
|
+
.controls input[type="search"]::-webkit-search-cancel-button {
|
|
898
|
+
appearance: none;
|
|
899
|
+
display: none;
|
|
900
|
+
}
|
|
880
901
|
/* One line, always. Wrapping was the defect: at a narrow width the
|
|
881
902
|
row dropped `deprecated` onto a second line, because no chip can
|
|
882
903
|
shrink below its own longest word and the rail's chip count is a
|
|
@@ -1026,11 +1047,6 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1026
1047
|
.sort-field:focus-visible {
|
|
1027
1048
|
z-index: 1;
|
|
1028
1049
|
}
|
|
1029
|
-
.sort-dir:focus-visible,
|
|
1030
|
-
.sort-field:focus-visible {
|
|
1031
|
-
outline: 2px solid var(--grim-color-accent);
|
|
1032
|
-
outline-offset: 1px;
|
|
1033
|
-
}
|
|
1034
1050
|
/* One box for every chip in the filter row — the kinds, the keywords,
|
|
1035
1051
|
the deprecated toggle and the overflow menu's trigger alike. The
|
|
1036
1052
|
line-height is stated rather than inherited because a `<summary>`
|
|
@@ -1062,21 +1078,13 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1062
1078
|
background: var(--grim-color-accent-soft);
|
|
1063
1079
|
}
|
|
1064
1080
|
/* 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
|
-
}
|
|
1081
|
+
what is on screen — and the chips slide rather than jump. There is
|
|
1082
|
+
deliberately no `translate` transition here: the FLIP effect in
|
|
1083
|
+
`Catalog.tsx` runs each slide as a Web Animation instead, reading
|
|
1084
|
+
`--grim-duration-slow` for its length and honouring
|
|
1085
|
+
`prefers-reduced-motion` itself. A transition needs an inline style
|
|
1086
|
+
to drive it, and an inline style is what a superseded pass used to
|
|
1087
|
+
leave behind on a chip stopped off its seat. */
|
|
1080
1088
|
/* The overflow menu. A popover, and it has to be one: `.filter-row`
|
|
1081
1089
|
above is a scroll container, and an absolutely-positioned panel
|
|
1082
1090
|
inside a scroll container is cropped to the row AND stretches the
|
|
@@ -1143,10 +1151,6 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1143
1151
|
font: inherit;
|
|
1144
1152
|
font-size: var(--grim-text-sm);
|
|
1145
1153
|
}
|
|
1146
|
-
.kw-menu-search:focus-visible {
|
|
1147
|
-
outline: 2px solid var(--grim-color-accent);
|
|
1148
|
-
outline-offset: 1px;
|
|
1149
|
-
}
|
|
1150
1154
|
.kw-menu-list {
|
|
1151
1155
|
display: flex;
|
|
1152
1156
|
flex-direction: column;
|
|
@@ -1240,9 +1244,38 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1240
1244
|
.view-pick.active {
|
|
1241
1245
|
z-index: 1;
|
|
1242
1246
|
}
|
|
1247
|
+
|
|
1248
|
+
/* One focus ring for every bordered control in the toolbar, and it is
|
|
1249
|
+
the card's: the border takes the accent and the outline sits
|
|
1250
|
+
directly on it, so the two merge into one thicker edge.
|
|
1251
|
+
|
|
1252
|
+
An offset ring is what put two borders on the sort combo. Hover
|
|
1253
|
+
already tints the border, so a ring standing 1px clear of it drew a
|
|
1254
|
+
second line in the same colour — one state reading as two. Every
|
|
1255
|
+
control here tints its border on hover or while active, so the
|
|
1256
|
+
offset had to go from all of them together rather than from the one
|
|
1257
|
+
that was reported. */
|
|
1258
|
+
.chip:focus-visible,
|
|
1259
|
+
.controls input[type="search"]:focus-visible,
|
|
1260
|
+
.kw-menu-search:focus-visible,
|
|
1261
|
+
.sort-dir:focus-visible,
|
|
1262
|
+
.sort-field:focus-visible,
|
|
1243
1263
|
.view-pick:focus-visible {
|
|
1244
1264
|
outline: 2px solid var(--grim-color-accent);
|
|
1245
|
-
outline-offset:
|
|
1265
|
+
outline-offset: 0;
|
|
1266
|
+
border-color: var(--grim-color-accent);
|
|
1267
|
+
}
|
|
1268
|
+
/* The sort combo, while a POINTER is driving it. Chromium hands a
|
|
1269
|
+
`<select>` `:focus-visible` on an ordinary click, so this control
|
|
1270
|
+
alone would wear the keyboard ring for a mouse interaction — around
|
|
1271
|
+
a closed box sitting behind its own open dropdown, which already
|
|
1272
|
+
says everything the ring would. `data-pointer` is set by the
|
|
1273
|
+
element's own `pointerdown` and cleared by a keypress or a blur, so
|
|
1274
|
+
the ring is only ever suppressed for the interaction that did not
|
|
1275
|
+
need it; every keyboard focus keeps it. */
|
|
1276
|
+
.sort-field[data-pointer]:focus-visible {
|
|
1277
|
+
outline: none;
|
|
1278
|
+
border-color: var(--grim-color-accent);
|
|
1246
1279
|
}
|
|
1247
1280
|
|
|
1248
1281
|
.grid {
|
|
@@ -1508,13 +1541,20 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1508
1541
|
the container rather than cutting square across it. */
|
|
1509
1542
|
.table {
|
|
1510
1543
|
display: grid;
|
|
1511
|
-
/* logo | kind | name | description | updated. The two
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1544
|
+
/* logo | kind | name | description | updated. The two marks lead,
|
|
1545
|
+
so the names start on one line down the whole list and the eye
|
|
1546
|
+
runs the column it actually reads.
|
|
1547
|
+
|
|
1548
|
+
Every track is sized to give the description whatever is left.
|
|
1549
|
+
The marks are `min-content` — a 20px tile and a 16px glyph, and
|
|
1550
|
+
nothing about them should reserve more than they draw. The name
|
|
1551
|
+
is `fit-content(14rem)`: as wide as the longest name in the list
|
|
1552
|
+
and no wider, capped, past which it clips. It was
|
|
1553
|
+
`minmax(8rem, 14rem)`, and that 8rem floor was a hole — a list of
|
|
1554
|
+
short names paid a fixed 8rem for them, so the description read
|
|
1555
|
+
as pushed away from a column of empty space. */
|
|
1556
|
+
grid-template-columns:
|
|
1557
|
+
min-content min-content fit-content(14rem) 1fr auto;
|
|
1518
1558
|
background: var(--grim-color-card);
|
|
1519
1559
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1520
1560
|
/* `xl`, the content-surface step — the same one the card wears.
|
|
@@ -1528,20 +1568,45 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1528
1568
|
/* One more track when the index publishes ratings. Decided once per
|
|
1529
1569
|
index, never per filter — see `PackageTable`. */
|
|
1530
1570
|
.table.rated {
|
|
1531
|
-
grid-template-columns:
|
|
1571
|
+
grid-template-columns:
|
|
1572
|
+
min-content min-content fit-content(14rem) 1fr auto auto;
|
|
1532
1573
|
}
|
|
1533
1574
|
.table .row {
|
|
1534
1575
|
grid-column: 1 / -1;
|
|
1535
1576
|
display: grid;
|
|
1536
1577
|
grid-template-columns: subgrid;
|
|
1537
1578
|
align-items: center;
|
|
1538
|
-
|
|
1579
|
+
/* One step tighter than the row's own padding. A 12px gutter either
|
|
1580
|
+
side of a 16px glyph reads as a hole rather than as spacing. */
|
|
1581
|
+
gap: var(--grim-space-4);
|
|
1539
1582
|
padding: var(--grim-space-3) var(--grim-space-5);
|
|
1540
1583
|
color: inherit;
|
|
1541
1584
|
text-decoration: none;
|
|
1542
1585
|
font-size: var(--grim-text-sm);
|
|
1543
1586
|
transition: background-color var(--grim-duration-base) ease;
|
|
1544
1587
|
}
|
|
1588
|
+
/* NO `content-visibility` on a row, and the reason is load-bearing.
|
|
1589
|
+
`content-visibility: auto` applies SIZE CONTAINMENT, and a
|
|
1590
|
+
size-contained element cannot be a subgrid — the `subgrid` above
|
|
1591
|
+
degrades to `none`, silently. Measured in Chromium 152 against a
|
|
1592
|
+
real build: with it, `getComputedStyle(row).gridTemplateColumns`
|
|
1593
|
+
reads `none` and every cell is one sixth of the table (181px each
|
|
1594
|
+
at a 1280px viewport); without it the parent's tracks apply and the
|
|
1595
|
+
description gets the 832px the `1fr` exists to give it.
|
|
1596
|
+
|
|
1597
|
+
It shipped in 2a77c15 as a paint bound, and the check made at the
|
|
1598
|
+
time — first and last rows' cells at identical offsets, before and
|
|
1599
|
+
after scrolling — could not have caught this: when EVERY row is the
|
|
1600
|
+
same independently-computed grid, the offsets match perfectly and
|
|
1601
|
+
the alignment is fake. Any check here must read the row's own
|
|
1602
|
+
`gridTemplateColumns`, or a cell's width against its content, never
|
|
1603
|
+
two rows against each other. `style_contract.test.ts` holds the
|
|
1604
|
+
text form of the invariant.
|
|
1605
|
+
|
|
1606
|
+
What that commit bought is not lost. The logo fetches it was aimed
|
|
1607
|
+
at are held off by `loading="lazy"` on the image itself, and the
|
|
1608
|
+
list's real cost — building every row — is bounded by the window in
|
|
1609
|
+
`Catalog.tsx`, which is that same commit's other half. */
|
|
1545
1610
|
/* The rule belongs between rows, not under the last one — which the
|
|
1546
1611
|
container's own border already draws. */
|
|
1547
1612
|
.table .row + .row {
|
|
@@ -1687,10 +1752,11 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1687
1752
|
@media (max-width: 40rem) {
|
|
1688
1753
|
.table,
|
|
1689
1754
|
.table.rated {
|
|
1690
|
-
grid-template-columns:
|
|
1755
|
+
grid-template-columns: min-content min-content minmax(6rem, 1fr) auto;
|
|
1691
1756
|
}
|
|
1692
1757
|
.table.rated {
|
|
1693
|
-
grid-template-columns:
|
|
1758
|
+
grid-template-columns:
|
|
1759
|
+
min-content min-content minmax(6rem, 1fr) auto auto;
|
|
1694
1760
|
}
|
|
1695
1761
|
.t-desc {
|
|
1696
1762
|
display: none;
|
|
@@ -1745,6 +1811,34 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1745
1811
|
font-size: var(--grim-text-xs);
|
|
1746
1812
|
line-height: 1.3;
|
|
1747
1813
|
font-family: ui-monospace, "SFMono-Regular", monospace;
|
|
1814
|
+
/* One line, whatever the address is. A namespace breaks at its own
|
|
1815
|
+
slashes and dots, so left to itself a long one wrapped to a second
|
|
1816
|
+
line and made the card's head taller than its neighbours'. */
|
|
1817
|
+
display: flex;
|
|
1818
|
+
min-width: 0;
|
|
1819
|
+
white-space: nowrap;
|
|
1820
|
+
}
|
|
1821
|
+
/* Trimmed from the FRONT: `github.com/` is on every address in an
|
|
1822
|
+
index and the repository at the tail is what tells two apart, so the
|
|
1823
|
+
head is what to spend.
|
|
1824
|
+
|
|
1825
|
+
`direction: rtl` is the mechanism — `text-overflow` ellipsises at
|
|
1826
|
+
the line's inline END, which in an RTL box is its left edge, and
|
|
1827
|
+
`text-align: left` keeps a short address sitting under the name
|
|
1828
|
+
rather than flung to the right. The address itself still reads
|
|
1829
|
+
left-to-right: it is one run of Latin characters, so the bidi
|
|
1830
|
+
algorithm lays it out LTR inside the RTL box. That holds because a
|
|
1831
|
+
namespace neither starts nor ends with punctuation — one that did
|
|
1832
|
+
would have that character reordered to the far end.
|
|
1833
|
+
|
|
1834
|
+
The two-value `text-overflow: ellipsis ""` would say this directly
|
|
1835
|
+
and is Firefox-only. */
|
|
1836
|
+
.namespace .address {
|
|
1837
|
+
min-width: 0;
|
|
1838
|
+
overflow: hidden;
|
|
1839
|
+
text-overflow: ellipsis;
|
|
1840
|
+
direction: rtl;
|
|
1841
|
+
text-align: left;
|
|
1748
1842
|
}
|
|
1749
1843
|
/* The rating is one control with two halves — the count, which opens
|
|
1750
1844
|
the forge thread, and the vote, which casts one — joined the way
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import type { CatalogPackage } from "../../types.js";
|
|
2
2
|
export type { CatalogPackage } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The fields the CATALOG ISLAND may read — and, because the island's props
|
|
5
|
+
* are serialized into the page, the only fields that are worth shipping.
|
|
6
|
+
*
|
|
7
|
+
* `index.astro` projects every package through `cardPackage()` before handing
|
|
8
|
+
* the array to `<Catalog>`. Astro serializes island props into an HTML
|
|
9
|
+
* attribute on `<astro-island>`, so anything left in a record is paid for
|
|
10
|
+
* twice on the landing page: once in the server-rendered card markup, and
|
|
11
|
+
* again as escaped JSON the browser parses at hydration. At a corporate-sized
|
|
12
|
+
* catalog the untrimmed record set was 323 KB of that attribute, ~18% of the
|
|
13
|
+
* page, most of it fields no card or row ever looks at — `license`,
|
|
14
|
+
* `authors`, `vendor`, `documentation`, `compatibility`, `revision`,
|
|
15
|
+
* `support`, `repository`, and every unknown enrichment key riding
|
|
16
|
+
* `IndexRecord`'s index signature.
|
|
17
|
+
*
|
|
18
|
+
* This is a TYPE, not just a filter, and that is the point: a component that
|
|
19
|
+
* reaches for a field not listed here fails to compile, rather than silently
|
|
20
|
+
* rendering `undefined` in the browser while the server render — which reads
|
|
21
|
+
* the full record — looks correct. The detail pages are unaffected; they read
|
|
22
|
+
* `data` directly and hydrate nothing.
|
|
23
|
+
*/
|
|
24
|
+
export type CardPackage = Pick<CatalogPackage, "name" | "kind" | "ref" | "namespace" | "description" | "summary" | "version" | "keywords" | "created" | "updated" | "deprecated" | "replacedBy" | "logo" | "rating">;
|
|
25
|
+
/**
|
|
26
|
+
* One package, trimmed to what the island renders.
|
|
27
|
+
*
|
|
28
|
+
* An absent key is omitted rather than set to `undefined`: `JSON.stringify`
|
|
29
|
+
* drops an undefined value anyway, and omitting it keeps the projection's
|
|
30
|
+
* output identical to what a hand-written object literal would produce.
|
|
31
|
+
*/
|
|
32
|
+
export declare function cardPackage(p: CatalogPackage): CardPackage;
|
|
3
33
|
/**
|
|
4
34
|
* When a package last moved, for sorting and for the "updated" stamp.
|
|
5
35
|
*
|
|
@@ -9,7 +39,7 @@ export type { CatalogPackage } from "../../types.js";
|
|
|
9
39
|
* existed: those records still date correctly instead of dropping into the
|
|
10
40
|
* unknown bucket on the first build after an upgrade.
|
|
11
41
|
*/
|
|
12
|
-
export declare function lastUpdated(p: CatalogPackage): string | undefined;
|
|
42
|
+
export declare function lastUpdated(p: Pick<CatalogPackage, "updated" | "created">): string | undefined;
|
|
13
43
|
/**
|
|
14
44
|
* The deprecation sentence, or `null` for a package that is not deprecated.
|
|
15
45
|
*
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/** The keys of `CardPackage`, as one list the projection actually iterates. */
|
|
4
|
+
const CARD_FIELDS = [
|
|
5
|
+
"name",
|
|
6
|
+
"kind",
|
|
7
|
+
"ref",
|
|
8
|
+
"namespace",
|
|
9
|
+
"description",
|
|
10
|
+
"summary",
|
|
11
|
+
"version",
|
|
12
|
+
"keywords",
|
|
13
|
+
"created",
|
|
14
|
+
"updated",
|
|
15
|
+
"deprecated",
|
|
16
|
+
"replacedBy",
|
|
17
|
+
"logo",
|
|
18
|
+
"rating",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* One package, trimmed to what the island renders.
|
|
22
|
+
*
|
|
23
|
+
* An absent key is omitted rather than set to `undefined`: `JSON.stringify`
|
|
24
|
+
* drops an undefined value anyway, and omitting it keeps the projection's
|
|
25
|
+
* output identical to what a hand-written object literal would produce.
|
|
26
|
+
*/
|
|
27
|
+
export function cardPackage(p) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const key of CARD_FIELDS) {
|
|
30
|
+
if (p[key] !== undefined)
|
|
31
|
+
out[key] = p[key];
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
3
35
|
/**
|
|
4
36
|
* When a package last moved, for sorting and for the "updated" stamp.
|
|
5
37
|
*
|
|
@@ -9,6 +9,78 @@ import type { CatalogPackage } from "../../types.js";
|
|
|
9
9
|
|
|
10
10
|
export type { CatalogPackage } from "../../types.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The fields the CATALOG ISLAND may read — and, because the island's props
|
|
14
|
+
* are serialized into the page, the only fields that are worth shipping.
|
|
15
|
+
*
|
|
16
|
+
* `index.astro` projects every package through `cardPackage()` before handing
|
|
17
|
+
* the array to `<Catalog>`. Astro serializes island props into an HTML
|
|
18
|
+
* attribute on `<astro-island>`, so anything left in a record is paid for
|
|
19
|
+
* twice on the landing page: once in the server-rendered card markup, and
|
|
20
|
+
* again as escaped JSON the browser parses at hydration. At a corporate-sized
|
|
21
|
+
* catalog the untrimmed record set was 323 KB of that attribute, ~18% of the
|
|
22
|
+
* page, most of it fields no card or row ever looks at — `license`,
|
|
23
|
+
* `authors`, `vendor`, `documentation`, `compatibility`, `revision`,
|
|
24
|
+
* `support`, `repository`, and every unknown enrichment key riding
|
|
25
|
+
* `IndexRecord`'s index signature.
|
|
26
|
+
*
|
|
27
|
+
* This is a TYPE, not just a filter, and that is the point: a component that
|
|
28
|
+
* reaches for a field not listed here fails to compile, rather than silently
|
|
29
|
+
* rendering `undefined` in the browser while the server render — which reads
|
|
30
|
+
* the full record — looks correct. The detail pages are unaffected; they read
|
|
31
|
+
* `data` directly and hydrate nothing.
|
|
32
|
+
*/
|
|
33
|
+
export type CardPackage = Pick<
|
|
34
|
+
CatalogPackage,
|
|
35
|
+
| "name"
|
|
36
|
+
| "kind"
|
|
37
|
+
| "ref"
|
|
38
|
+
| "namespace"
|
|
39
|
+
| "description"
|
|
40
|
+
| "summary"
|
|
41
|
+
| "version"
|
|
42
|
+
| "keywords"
|
|
43
|
+
| "created"
|
|
44
|
+
| "updated"
|
|
45
|
+
| "deprecated"
|
|
46
|
+
| "replacedBy"
|
|
47
|
+
| "logo"
|
|
48
|
+
| "rating"
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
/** The keys of `CardPackage`, as one list the projection actually iterates. */
|
|
52
|
+
const CARD_FIELDS = [
|
|
53
|
+
"name",
|
|
54
|
+
"kind",
|
|
55
|
+
"ref",
|
|
56
|
+
"namespace",
|
|
57
|
+
"description",
|
|
58
|
+
"summary",
|
|
59
|
+
"version",
|
|
60
|
+
"keywords",
|
|
61
|
+
"created",
|
|
62
|
+
"updated",
|
|
63
|
+
"deprecated",
|
|
64
|
+
"replacedBy",
|
|
65
|
+
"logo",
|
|
66
|
+
"rating",
|
|
67
|
+
] as const satisfies readonly (keyof CardPackage)[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One package, trimmed to what the island renders.
|
|
71
|
+
*
|
|
72
|
+
* An absent key is omitted rather than set to `undefined`: `JSON.stringify`
|
|
73
|
+
* drops an undefined value anyway, and omitting it keeps the projection's
|
|
74
|
+
* output identical to what a hand-written object literal would produce.
|
|
75
|
+
*/
|
|
76
|
+
export function cardPackage(p: CatalogPackage): CardPackage {
|
|
77
|
+
const out: Record<string, unknown> = {};
|
|
78
|
+
for (const key of CARD_FIELDS) {
|
|
79
|
+
if (p[key] !== undefined) out[key] = p[key];
|
|
80
|
+
}
|
|
81
|
+
return out as CardPackage;
|
|
82
|
+
}
|
|
83
|
+
|
|
12
84
|
/**
|
|
13
85
|
* When a package last moved, for sorting and for the "updated" stamp.
|
|
14
86
|
*
|
|
@@ -18,7 +90,7 @@ export type { CatalogPackage } from "../../types.js";
|
|
|
18
90
|
* existed: those records still date correctly instead of dropping into the
|
|
19
91
|
* unknown bucket on the first build after an upgrade.
|
|
20
92
|
*/
|
|
21
|
-
export function lastUpdated(p: CatalogPackage): string | undefined {
|
|
93
|
+
export function lastUpdated(p: Pick<CatalogPackage, "updated" | "created">): string | undefined {
|
|
22
94
|
return p.updated ?? p.created;
|
|
23
95
|
}
|
|
24
96
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Every package that matched, by `ref`, with its 0–1 score. */
|
|
2
|
+
export type Scores = ReadonlyMap<string, number>;
|
|
3
|
+
export interface SearchIndex {
|
|
4
|
+
/** Score one query against the whole catalog. Empty query, empty map. */
|
|
5
|
+
search(query: string): Scores;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetch the full catalog and build the matcher over it.
|
|
9
|
+
*
|
|
10
|
+
* Rejects rather than degrading quietly — the caller decides what a failure
|
|
11
|
+
* means, and for the island it means "keep the substring filter". Records
|
|
12
|
+
* without a string `ref` are dropped instead of failing the whole load: `ref`
|
|
13
|
+
* is how a hit is joined back onto the card the island already holds, so a
|
|
14
|
+
* record without one could never be shown even if it matched.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadSearchIndex(url: string): Promise<SearchIndex>;
|
|
17
|
+
//# sourceMappingURL=search.d.ts.map
|