@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +250 -270
  2. package/README.md +26 -0
  3. package/dist/cli/ratings.d.ts.map +1 -1
  4. package/dist/cli/ratings.js +29 -1
  5. package/dist/cli/ratings.js.map +1 -1
  6. package/dist/renderer/astro/components/CardLogo.d.ts +2 -2
  7. package/dist/renderer/astro/components/CardLogo.tsx +2 -2
  8. package/dist/renderer/astro/components/Catalog.d.ts +12 -4
  9. package/dist/renderer/astro/components/Catalog.js +290 -38
  10. package/dist/renderer/astro/components/Catalog.tsx +369 -57
  11. package/dist/renderer/astro/components/PackageCard.d.ts +2 -2
  12. package/dist/renderer/astro/components/PackageCard.tsx +2 -2
  13. package/dist/renderer/astro/components/PackageRow.d.ts +2 -2
  14. package/dist/renderer/astro/components/PackageRow.tsx +2 -2
  15. package/dist/renderer/astro/components/PickerMenu.astro +1 -0
  16. package/dist/renderer/astro/layouts/Base.astro +98 -22
  17. package/dist/renderer/astro/lib/catalog.d.ts +39 -2
  18. package/dist/renderer/astro/lib/catalog.js +42 -1
  19. package/dist/renderer/astro/lib/catalog.ts +82 -1
  20. package/dist/renderer/astro/lib/commands.d.ts +7 -0
  21. package/dist/renderer/astro/lib/commands.js +9 -2
  22. package/dist/renderer/astro/lib/commands.ts +17 -2
  23. package/dist/renderer/astro/lib/search.d.ts +17 -0
  24. package/dist/renderer/astro/lib/search.js +138 -0
  25. package/dist/renderer/astro/lib/search.ts +157 -0
  26. package/dist/renderer/astro/pages/index.astro +51 -3
  27. package/dist/renderer/astro/styles/tokens.css +6 -1
  28. package/dist/renderer/index.d.ts.map +1 -1
  29. package/dist/renderer/index.js +52 -0
  30. package/dist/renderer/index.js.map +1 -1
  31. 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 — is
66
- // the wrong one. Flag it so CSS holds the catalog back rather than
67
- // showing the full list and rearranging it a moment later. The island
68
- // drops the flag once it has applied the view — which is the render
69
- // after hydration, never the hydrating one itself; the timeout is the
70
- // failsafe for an island that never hydrates, where a flash beats a
71
- // blank page.
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) => pref("grim.catalog." + k));
74
- if (view.get("q") || view.get("kind") || arranged) {
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 fixed-width
1508
- marks lead, so the names start on one line down the whole list
1509
- and the eye runs the column it actually reads. The name is
1510
- capped rather than `max-content`: an unbounded name ate the
1511
- description's space, and past the cap both clip instead. The
1512
- description is the only track that grows. */
1513
- grid-template-columns: auto auto minmax(8rem, 14rem) 1fr auto;
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: auto auto minmax(8rem, 14rem) 1fr auto auto;
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
- gap: var(--grim-space-5);
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: auto auto minmax(6rem, 1fr) auto;
1761
+ grid-template-columns: min-content min-content minmax(6rem, 1fr) auto;
1687
1762
  }
1688
1763
  .table.rated {
1689
- grid-template-columns: auto auto minmax(6rem, 1fr) auto auto;
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
- { name: "Global", command: `grim --global ${add.slice("grim ".length)}`, Icon: Globe },
79
- { name: "Project", command: add, Icon: FolderRoot },
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
- { name: "Global", command: `grim --global ${add.slice("grim ".length)}`, Icon: Globe },
120
- { name: "Project", command: add, Icon: FolderRoot },
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