@grimoire-rs/indexer 0.5.2 → 0.5.4
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 +250 -270
- package/README.md +26 -0
- package/dist/cli/ratings.d.ts.map +1 -1
- package/dist/cli/ratings.js +29 -1
- package/dist/cli/ratings.js.map +1 -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 +290 -38
- package/dist/renderer/astro/components/Catalog.tsx +369 -57
- package/dist/renderer/astro/components/PackageCard.d.ts +2 -2
- package/dist/renderer/astro/components/PackageCard.tsx +2 -2
- package/dist/renderer/astro/components/PackageRow.d.ts +2 -2
- package/dist/renderer/astro/components/PackageRow.tsx +2 -2
- package/dist/renderer/astro/components/PickerMenu.astro +1 -0
- package/dist/renderer/astro/layouts/Base.astro +98 -22
- package/dist/renderer/astro/lib/catalog.d.ts +39 -2
- package/dist/renderer/astro/lib/catalog.js +42 -1
- package/dist/renderer/astro/lib/catalog.ts +82 -1
- package/dist/renderer/astro/lib/commands.d.ts +7 -0
- package/dist/renderer/astro/lib/commands.js +9 -2
- package/dist/renderer/astro/lib/commands.ts +17 -2
- 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 +51 -3
- 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
|
@@ -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
|
}
|
|
@@ -102,6 +104,12 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
102
104
|
for (const glyph of bar.querySelectorAll("[data-os-glyph]")) {
|
|
103
105
|
glyph.hidden = glyph.dataset.osGlyph !== pick.dataset.osName;
|
|
104
106
|
}
|
|
107
|
+
// The action segment follows the choice where it has one to follow:
|
|
108
|
+
// the registry bar's VS Code link carries the scope the command names,
|
|
109
|
+
// so picking Global cannot leave the button writing project config.
|
|
110
|
+
// A choice with no href leaves the link alone rather than blanking it.
|
|
111
|
+
const link = bar.querySelector("[data-os-link]");
|
|
112
|
+
if (link && pick.dataset.osHref) link.href = pick.dataset.osHref;
|
|
105
113
|
};
|
|
106
114
|
|
|
107
115
|
// The platform a bar should start on, or null for one this visitor's
|
|
@@ -853,12 +861,12 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
853
861
|
that is not yet in use, so it withdraws the moment the field is
|
|
854
862
|
focused or holds a query — at which point it would only be
|
|
855
863
|
sitting on top of what is being typed. */
|
|
856
|
-
.search-hint
|
|
864
|
+
.search-hint,
|
|
865
|
+
.search-clear {
|
|
857
866
|
position: absolute;
|
|
858
867
|
right: 0.5rem;
|
|
859
868
|
top: 50%;
|
|
860
869
|
translate: 0 -50%;
|
|
861
|
-
pointer-events: none;
|
|
862
870
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
863
871
|
border-bottom-width: 2px;
|
|
864
872
|
border-radius: var(--grim-radius-inset);
|
|
@@ -869,10 +877,33 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
869
877
|
line-height: 1;
|
|
870
878
|
padding: var(--grim-space-2) var(--grim-space-3);
|
|
871
879
|
}
|
|
880
|
+
.search-hint {
|
|
881
|
+
pointer-events: none;
|
|
882
|
+
}
|
|
883
|
+
/* The two never coexist — the hint withdraws exactly when a query
|
|
884
|
+
appears, which is when the clear button is rendered — so they share
|
|
885
|
+
one corner rather than sitting beside each other. */
|
|
872
886
|
.search-field:focus-within .search-hint,
|
|
873
887
|
.search-field:has(input:not(:placeholder-shown)) .search-hint {
|
|
874
888
|
display: none;
|
|
875
889
|
}
|
|
890
|
+
/* Sized to the glyph rather than to a text line, so the box matches
|
|
891
|
+
the hint's height instead of growing around an SVG on a baseline. */
|
|
892
|
+
.search-clear {
|
|
893
|
+
display: grid;
|
|
894
|
+
place-items: center;
|
|
895
|
+
cursor: pointer;
|
|
896
|
+
}
|
|
897
|
+
.search-clear:hover {
|
|
898
|
+
color: var(--grim-color-fg);
|
|
899
|
+
}
|
|
900
|
+
/* The UA's own clear button, gone: it renders in this same corner in
|
|
901
|
+
Chromium and Safari, in a style that answers to no token here, and
|
|
902
|
+
it cannot be restyled beyond hiding. Ours replaces it. */
|
|
903
|
+
.controls input[type="search"]::-webkit-search-cancel-button {
|
|
904
|
+
appearance: none;
|
|
905
|
+
display: none;
|
|
906
|
+
}
|
|
876
907
|
/* One line, always. Wrapping was the defect: at a narrow width the
|
|
877
908
|
row dropped `deprecated` onto a second line, because no chip can
|
|
878
909
|
shrink below its own longest word and the rail's chip count is a
|
|
@@ -1240,6 +1271,18 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1240
1271
|
outline-offset: 0;
|
|
1241
1272
|
border-color: var(--grim-color-accent);
|
|
1242
1273
|
}
|
|
1274
|
+
/* The sort combo, while a POINTER is driving it. Chromium hands a
|
|
1275
|
+
`<select>` `:focus-visible` on an ordinary click, so this control
|
|
1276
|
+
alone would wear the keyboard ring for a mouse interaction — around
|
|
1277
|
+
a closed box sitting behind its own open dropdown, which already
|
|
1278
|
+
says everything the ring would. `data-pointer` is set by the
|
|
1279
|
+
element's own `pointerdown` and cleared by a keypress or a blur, so
|
|
1280
|
+
the ring is only ever suppressed for the interaction that did not
|
|
1281
|
+
need it; every keyboard focus keeps it. */
|
|
1282
|
+
.sort-field[data-pointer]:focus-visible {
|
|
1283
|
+
outline: none;
|
|
1284
|
+
border-color: var(--grim-color-accent);
|
|
1285
|
+
}
|
|
1243
1286
|
|
|
1244
1287
|
.grid {
|
|
1245
1288
|
list-style: none;
|
|
@@ -1504,13 +1547,20 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1504
1547
|
the container rather than cutting square across it. */
|
|
1505
1548
|
.table {
|
|
1506
1549
|
display: grid;
|
|
1507
|
-
/* logo | kind | name | description | updated. The two
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1550
|
+
/* logo | kind | name | description | updated. The two marks lead,
|
|
1551
|
+
so the names start on one line down the whole list and the eye
|
|
1552
|
+
runs the column it actually reads.
|
|
1553
|
+
|
|
1554
|
+
Every track is sized to give the description whatever is left.
|
|
1555
|
+
The marks are `min-content` — a 20px tile and a 16px glyph, and
|
|
1556
|
+
nothing about them should reserve more than they draw. The name
|
|
1557
|
+
is `fit-content(14rem)`: as wide as the longest name in the list
|
|
1558
|
+
and no wider, capped, past which it clips. It was
|
|
1559
|
+
`minmax(8rem, 14rem)`, and that 8rem floor was a hole — a list of
|
|
1560
|
+
short names paid a fixed 8rem for them, so the description read
|
|
1561
|
+
as pushed away from a column of empty space. */
|
|
1562
|
+
grid-template-columns:
|
|
1563
|
+
min-content min-content fit-content(14rem) 1fr auto;
|
|
1514
1564
|
background: var(--grim-color-card);
|
|
1515
1565
|
border: var(--grim-border-width) solid var(--grim-color-border);
|
|
1516
1566
|
/* `xl`, the content-surface step — the same one the card wears.
|
|
@@ -1524,20 +1574,45 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1524
1574
|
/* One more track when the index publishes ratings. Decided once per
|
|
1525
1575
|
index, never per filter — see `PackageTable`. */
|
|
1526
1576
|
.table.rated {
|
|
1527
|
-
grid-template-columns:
|
|
1577
|
+
grid-template-columns:
|
|
1578
|
+
min-content min-content fit-content(14rem) 1fr auto auto;
|
|
1528
1579
|
}
|
|
1529
1580
|
.table .row {
|
|
1530
1581
|
grid-column: 1 / -1;
|
|
1531
1582
|
display: grid;
|
|
1532
1583
|
grid-template-columns: subgrid;
|
|
1533
1584
|
align-items: center;
|
|
1534
|
-
|
|
1585
|
+
/* One step tighter than the row's own padding. A 12px gutter either
|
|
1586
|
+
side of a 16px glyph reads as a hole rather than as spacing. */
|
|
1587
|
+
gap: var(--grim-space-4);
|
|
1535
1588
|
padding: var(--grim-space-3) var(--grim-space-5);
|
|
1536
1589
|
color: inherit;
|
|
1537
1590
|
text-decoration: none;
|
|
1538
1591
|
font-size: var(--grim-text-sm);
|
|
1539
1592
|
transition: background-color var(--grim-duration-base) ease;
|
|
1540
1593
|
}
|
|
1594
|
+
/* NO `content-visibility` on a row, and the reason is load-bearing.
|
|
1595
|
+
`content-visibility: auto` applies SIZE CONTAINMENT, and a
|
|
1596
|
+
size-contained element cannot be a subgrid — the `subgrid` above
|
|
1597
|
+
degrades to `none`, silently. Measured in Chromium 152 against a
|
|
1598
|
+
real build: with it, `getComputedStyle(row).gridTemplateColumns`
|
|
1599
|
+
reads `none` and every cell is one sixth of the table (181px each
|
|
1600
|
+
at a 1280px viewport); without it the parent's tracks apply and the
|
|
1601
|
+
description gets the 832px the `1fr` exists to give it.
|
|
1602
|
+
|
|
1603
|
+
It shipped in 2a77c15 as a paint bound, and the check made at the
|
|
1604
|
+
time — first and last rows' cells at identical offsets, before and
|
|
1605
|
+
after scrolling — could not have caught this: when EVERY row is the
|
|
1606
|
+
same independently-computed grid, the offsets match perfectly and
|
|
1607
|
+
the alignment is fake. Any check here must read the row's own
|
|
1608
|
+
`gridTemplateColumns`, or a cell's width against its content, never
|
|
1609
|
+
two rows against each other. `style_contract.test.ts` holds the
|
|
1610
|
+
text form of the invariant.
|
|
1611
|
+
|
|
1612
|
+
What that commit bought is not lost. The logo fetches it was aimed
|
|
1613
|
+
at are held off by `loading="lazy"` on the image itself, and the
|
|
1614
|
+
list's real cost — building every row — is bounded by the window in
|
|
1615
|
+
`Catalog.tsx`, which is that same commit's other half. */
|
|
1541
1616
|
/* The rule belongs between rows, not under the last one — which the
|
|
1542
1617
|
container's own border already draws. */
|
|
1543
1618
|
.table .row + .row {
|
|
@@ -1683,10 +1758,11 @@ const { title, description, image = config.logo ?? config.favicon } = Astro.prop
|
|
|
1683
1758
|
@media (max-width: 40rem) {
|
|
1684
1759
|
.table,
|
|
1685
1760
|
.table.rated {
|
|
1686
|
-
grid-template-columns:
|
|
1761
|
+
grid-template-columns: min-content min-content minmax(6rem, 1fr) auto;
|
|
1687
1762
|
}
|
|
1688
1763
|
.table.rated {
|
|
1689
|
-
grid-template-columns:
|
|
1764
|
+
grid-template-columns:
|
|
1765
|
+
min-content min-content minmax(6rem, 1fr) auto auto;
|
|
1690
1766
|
}
|
|
1691
1767
|
.t-desc {
|
|
1692
1768
|
display: none;
|
|
@@ -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
|
*
|
|
@@ -57,6 +87,13 @@ export declare function vscodeVoteUrl(extension: string | null, ref: string): st
|
|
|
57
87
|
* `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
|
|
58
88
|
* one-click counterpart of the `grim config registry add` line beside it.
|
|
59
89
|
*
|
|
90
|
+
* `scope` carries the Scope picker sitting in the same bar, so the button
|
|
91
|
+
* writes where the copyable command says it would. The extension clamps it
|
|
92
|
+
* rather than trusting it — with no folder open the write is global whatever
|
|
93
|
+
* the link asked for — and words its confirmation modal off the RESULT, so a
|
|
94
|
+
* page steering a machine-wide write is what the user reads before agreeing.
|
|
95
|
+
* Omitting it leaves the extension on its own host-derived default.
|
|
96
|
+
*
|
|
60
97
|
* Null unless the link would actually work: the handler takes https only (an
|
|
61
98
|
* index locator is fetched with whatever credentials the user configures for
|
|
62
99
|
* it), refuses embedded credentials, and caps the URL at 2048 characters.
|
|
@@ -66,7 +103,7 @@ export declare function vscodeVoteUrl(extension: string | null, ref: string): st
|
|
|
66
103
|
export declare function addRegistryUrl(extension: string | null, registry: {
|
|
67
104
|
alias: string;
|
|
68
105
|
index: string;
|
|
69
|
-
} | null): string | null;
|
|
106
|
+
} | null, scope?: "global" | "project"): string | null;
|
|
70
107
|
/**
|
|
71
108
|
* A bundle member's `id` resolved to the reference the index stores.
|
|
72
109
|
*
|
|
@@ -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
|
*
|
|
@@ -151,13 +183,20 @@ const REGISTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
|
151
183
|
* `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
|
|
152
184
|
* one-click counterpart of the `grim config registry add` line beside it.
|
|
153
185
|
*
|
|
186
|
+
* `scope` carries the Scope picker sitting in the same bar, so the button
|
|
187
|
+
* writes where the copyable command says it would. The extension clamps it
|
|
188
|
+
* rather than trusting it — with no folder open the write is global whatever
|
|
189
|
+
* the link asked for — and words its confirmation modal off the RESULT, so a
|
|
190
|
+
* page steering a machine-wide write is what the user reads before agreeing.
|
|
191
|
+
* Omitting it leaves the extension on its own host-derived default.
|
|
192
|
+
*
|
|
154
193
|
* Null unless the link would actually work: the handler takes https only (an
|
|
155
194
|
* index locator is fetched with whatever credentials the user configures for
|
|
156
195
|
* it), refuses embedded credentials, and caps the URL at 2048 characters.
|
|
157
196
|
* Checking here keeps a broken button off the page rather than putting the
|
|
158
197
|
* failure in the user's hands.
|
|
159
198
|
*/
|
|
160
|
-
export function addRegistryUrl(extension, registry) {
|
|
199
|
+
export function addRegistryUrl(extension, registry, scope) {
|
|
161
200
|
if (!extension || !registry || !REGISTRY_ALIAS.test(registry.alias))
|
|
162
201
|
return null;
|
|
163
202
|
let url;
|
|
@@ -172,6 +211,8 @@ export function addRegistryUrl(extension, registry) {
|
|
|
172
211
|
if (url.href.length > 2048)
|
|
173
212
|
return null;
|
|
174
213
|
const query = new URLSearchParams({ index: url.href, alias: registry.alias });
|
|
214
|
+
if (scope)
|
|
215
|
+
query.set("scope", scope);
|
|
175
216
|
return `vscode://${extension}/add-registry?${query.toString()}`;
|
|
176
217
|
}
|
|
177
218
|
/**
|
|
@@ -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
|
|
|
@@ -166,6 +238,13 @@ const REGISTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
|
166
238
|
* `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
|
|
167
239
|
* one-click counterpart of the `grim config registry add` line beside it.
|
|
168
240
|
*
|
|
241
|
+
* `scope` carries the Scope picker sitting in the same bar, so the button
|
|
242
|
+
* writes where the copyable command says it would. The extension clamps it
|
|
243
|
+
* rather than trusting it — with no folder open the write is global whatever
|
|
244
|
+
* the link asked for — and words its confirmation modal off the RESULT, so a
|
|
245
|
+
* page steering a machine-wide write is what the user reads before agreeing.
|
|
246
|
+
* Omitting it leaves the extension on its own host-derived default.
|
|
247
|
+
*
|
|
169
248
|
* Null unless the link would actually work: the handler takes https only (an
|
|
170
249
|
* index locator is fetched with whatever credentials the user configures for
|
|
171
250
|
* it), refuses embedded credentials, and caps the URL at 2048 characters.
|
|
@@ -175,6 +254,7 @@ const REGISTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
|
175
254
|
export function addRegistryUrl(
|
|
176
255
|
extension: string | null,
|
|
177
256
|
registry: { alias: string; index: string } | null,
|
|
257
|
+
scope?: "global" | "project",
|
|
178
258
|
): string | null {
|
|
179
259
|
if (!extension || !registry || !REGISTRY_ALIAS.test(registry.alias)) return null;
|
|
180
260
|
let url: URL;
|
|
@@ -186,6 +266,7 @@ export function addRegistryUrl(
|
|
|
186
266
|
if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return null;
|
|
187
267
|
if (url.href.length > 2048) return null;
|
|
188
268
|
const query = new URLSearchParams({ index: url.href, alias: registry.alias });
|
|
269
|
+
if (scope) query.set("scope", scope);
|
|
189
270
|
return `vscode://${extension}/add-registry?${query.toString()}`;
|
|
190
271
|
}
|
|
191
272
|
|
|
@@ -20,6 +20,13 @@ export interface Choice {
|
|
|
20
20
|
Icon?: (props: {
|
|
21
21
|
size?: number;
|
|
22
22
|
}) => unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Where the bar's action segment should point once this choice is picked.
|
|
25
|
+
* Only the registry bar has one — a deep link that has to agree with the
|
|
26
|
+
* scope the command beside it names — and a bar whose choices carry none
|
|
27
|
+
* leaves its action link exactly as rendered.
|
|
28
|
+
*/
|
|
29
|
+
href?: string;
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* The installer choices, one per platform rather than one per config row.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// <CommandBar choices={installChoices(data.config)} noun="install command" … />
|
|
15
15
|
import { FolderRoot, Globe } from "lucide-preact";
|
|
16
16
|
import { mdiApple, mdiConsole, mdiMicrosoftWindows, mdiPenguin, } from "@mdi/js";
|
|
17
|
+
import { addRegistryUrl } from "./catalog.js";
|
|
17
18
|
/**
|
|
18
19
|
* `mdiPenguin`, not `mdiLinux`: Tux's own silhouette is a solid mass that
|
|
19
20
|
* reads as an indistinct blob at 16px, where the plainer penguin keeps its
|
|
@@ -74,9 +75,15 @@ export function registryScopeChoices(config, registry = config.registry) {
|
|
|
74
75
|
const add = registryAddCommand(config, registry);
|
|
75
76
|
if (!add)
|
|
76
77
|
return [];
|
|
78
|
+
const link = (scope) => addRegistryUrl(config.vscodeExtension, registry, scope) ?? undefined;
|
|
77
79
|
return [
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
+
{
|
|
81
|
+
name: "Global",
|
|
82
|
+
command: `grim --global ${add.slice("grim ".length)}`,
|
|
83
|
+
Icon: Globe,
|
|
84
|
+
href: link("global"),
|
|
85
|
+
},
|
|
86
|
+
{ name: "Project", command: add, Icon: FolderRoot, href: link("project") },
|
|
80
87
|
];
|
|
81
88
|
}
|
|
82
89
|
/**
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
mdiPenguin,
|
|
22
22
|
} from "@mdi/js";
|
|
23
23
|
import type { RegistryHint, ResolvedSiteConfig } from "../../../config.js";
|
|
24
|
+
import { addRegistryUrl } from "./catalog.js";
|
|
24
25
|
|
|
25
26
|
// Re-exported so an index's own page can name the shape it passes to the two
|
|
26
27
|
// functions below without reaching past `@grim/lib/*` into the package's
|
|
@@ -45,6 +46,13 @@ export interface Choice {
|
|
|
45
46
|
path?: string;
|
|
46
47
|
/** Lucide component, taking a `size` prop. */
|
|
47
48
|
Icon?: (props: { size?: number }) => unknown;
|
|
49
|
+
/**
|
|
50
|
+
* Where the bar's action segment should point once this choice is picked.
|
|
51
|
+
* Only the registry bar has one — a deep link that has to agree with the
|
|
52
|
+
* scope the command beside it names — and a bar whose choices carry none
|
|
53
|
+
* leaves its action link exactly as rendered.
|
|
54
|
+
*/
|
|
55
|
+
href?: string;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -115,9 +123,16 @@ export function registryScopeChoices(
|
|
|
115
123
|
): Choice[] {
|
|
116
124
|
const add = registryAddCommand(config, registry);
|
|
117
125
|
if (!add) return [];
|
|
126
|
+
const link = (scope: "global" | "project") =>
|
|
127
|
+
addRegistryUrl(config.vscodeExtension, registry, scope) ?? undefined;
|
|
118
128
|
return [
|
|
119
|
-
{
|
|
120
|
-
|
|
129
|
+
{
|
|
130
|
+
name: "Global",
|
|
131
|
+
command: `grim --global ${add.slice("grim ".length)}`,
|
|
132
|
+
Icon: Globe,
|
|
133
|
+
href: link("global"),
|
|
134
|
+
},
|
|
135
|
+
{ name: "Project", command: add, Icon: FolderRoot, href: link("project") },
|
|
121
136
|
];
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -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
|