@grimoire-rs/indexer 0.1.8 → 0.2.0

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 (42) hide show
  1. package/README.md +38 -0
  2. package/dist/cli/init.js +1 -1
  3. package/dist/cli/init.js.map +1 -1
  4. package/dist/config.d.ts +21 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +19 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/enrich/index.d.ts.map +1 -1
  9. package/dist/enrich/index.js +62 -0
  10. package/dist/enrich/index.js.map +1 -1
  11. package/dist/renderer/astro/components/BrandMark.d.ts +14 -0
  12. package/dist/renderer/astro/components/BrandMark.d.ts.map +1 -0
  13. package/dist/renderer/astro/components/BrandMark.js +16 -0
  14. package/dist/renderer/astro/components/BrandMark.js.map +1 -0
  15. package/dist/renderer/astro/components/BrandMark.tsx +19 -0
  16. package/dist/renderer/astro/components/Catalog.d.ts.map +1 -1
  17. package/dist/renderer/astro/components/Catalog.js +310 -18
  18. package/dist/renderer/astro/components/Catalog.js.map +1 -1
  19. package/dist/renderer/astro/components/Catalog.tsx +445 -63
  20. package/dist/renderer/astro/components/CommandField.astro +43 -0
  21. package/dist/renderer/astro/components/PickerMenu.astro +68 -0
  22. package/dist/renderer/astro/components/VersionMenu.astro +6 -2
  23. package/dist/renderer/astro/content.config.ts +23 -5
  24. package/dist/renderer/astro/layouts/Base.astro +794 -66
  25. package/dist/renderer/astro/lib/catalog.d.ts +24 -0
  26. package/dist/renderer/astro/lib/catalog.d.ts.map +1 -1
  27. package/dist/renderer/astro/lib/catalog.js +62 -0
  28. package/dist/renderer/astro/lib/catalog.js.map +1 -1
  29. package/dist/renderer/astro/lib/catalog.ts +62 -0
  30. package/dist/renderer/astro/lib/code.d.ts +22 -0
  31. package/dist/renderer/astro/lib/code.d.ts.map +1 -0
  32. package/dist/renderer/astro/lib/code.js +21 -0
  33. package/dist/renderer/astro/lib/code.js.map +1 -0
  34. package/dist/renderer/astro/lib/code.ts +21 -0
  35. package/dist/renderer/astro/pages/index.astro +144 -36
  36. package/dist/renderer/astro/pages/p/[...slug].astro +403 -55
  37. package/dist/renderer/index.d.ts +23 -0
  38. package/dist/renderer/index.d.ts.map +1 -1
  39. package/dist/renderer/index.js +117 -30
  40. package/dist/renderer/index.js.map +1 -1
  41. package/package.json +5 -1
  42. package/dist/renderer/astro/components/CopyButton.astro +0 -24
@@ -4,4 +4,28 @@ export declare function compareVersions(a: string, b: string): number;
4
4
  export declare function timeAgo(iso: string): string;
5
5
  /** `vscode://<publisher.extension>/open?repo=<ref>`, or null when disabled. */
6
6
  export declare function vscodeUrl(extension: string | null, ref: string): string | null;
7
+ /**
8
+ * `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
9
+ * one-click counterpart of the `grim config registry add` line beside it.
10
+ *
11
+ * Null unless the link would actually work: the handler takes https only (an
12
+ * index locator is fetched with whatever credentials the user configures for
13
+ * it), refuses embedded credentials, and caps the URL at 2048 characters.
14
+ * Checking here keeps a broken button off the page rather than putting the
15
+ * failure in the user's hands.
16
+ */
17
+ export declare function addRegistryUrl(extension: string | null, registry: {
18
+ alias: string;
19
+ index: string;
20
+ } | null): string | null;
21
+ /**
22
+ * A bundle member's `id` resolved to the reference the index stores.
23
+ *
24
+ * grim writes members relative to the bundle itself — `../skills/grim-usage:0`
25
+ * — so a member published alongside its bundle stays correct wherever the
26
+ * whole namespace is mirrored. The catalog keys on absolute, untagged refs,
27
+ * which is what this returns; an id that is already absolute passes through
28
+ * with only its tag or digest removed.
29
+ */
30
+ export declare function resolveMemberRef(bundleRef: string, id: string): string;
7
31
  //# sourceMappingURL=catalog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/catalog.ts"],"names":[],"mappings":"AAOA,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKrD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAWpF;AAID,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa5D;AAcD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS3C;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE9E"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/catalog.ts"],"names":[],"mappings":"AAOA,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKrD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAWpF;AAID,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa5D;AAcD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS3C;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE9E;AAUD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAChD,MAAM,GAAG,IAAI,CAYf;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAgBtE"}
@@ -58,4 +58,66 @@ export function timeAgo(iso) {
58
58
  export function vscodeUrl(extension, ref) {
59
59
  return extension ? `vscode://${extension}/open?repo=${encodeURIComponent(ref)}` : null;
60
60
  }
61
+ /**
62
+ * Alias charset the extension's `/add-registry` handler accepts. It has to be
63
+ * a TOML bare key and safe as a CLI argument, so it refuses a link rather
64
+ * than escaping one. Mirrored here so a config that cannot produce a working
65
+ * link renders no button at all, instead of one that silently does nothing.
66
+ */
67
+ const REGISTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
68
+ /**
69
+ * `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
70
+ * one-click counterpart of the `grim config registry add` line beside it.
71
+ *
72
+ * Null unless the link would actually work: the handler takes https only (an
73
+ * index locator is fetched with whatever credentials the user configures for
74
+ * it), refuses embedded credentials, and caps the URL at 2048 characters.
75
+ * Checking here keeps a broken button off the page rather than putting the
76
+ * failure in the user's hands.
77
+ */
78
+ export function addRegistryUrl(extension, registry) {
79
+ if (!extension || !registry || !REGISTRY_ALIAS.test(registry.alias))
80
+ return null;
81
+ let url;
82
+ try {
83
+ url = new URL(registry.index);
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "")
89
+ return null;
90
+ if (url.href.length > 2048)
91
+ return null;
92
+ const query = new URLSearchParams({ index: url.href, alias: registry.alias });
93
+ return `vscode://${extension}/add-registry?${query.toString()}`;
94
+ }
95
+ /**
96
+ * A bundle member's `id` resolved to the reference the index stores.
97
+ *
98
+ * grim writes members relative to the bundle itself — `../skills/grim-usage:0`
99
+ * — so a member published alongside its bundle stays correct wherever the
100
+ * whole namespace is mirrored. The catalog keys on absolute, untagged refs,
101
+ * which is what this returns; an id that is already absolute passes through
102
+ * with only its tag or digest removed.
103
+ */
104
+ export function resolveMemberRef(bundleRef, id) {
105
+ const pinned = id.split("@")[0] ?? id;
106
+ // A tag is a colon *after* the last slash — `localhost:5000/x` is a host,
107
+ // not a tag.
108
+ const slash = pinned.lastIndexOf("/");
109
+ const colon = pinned.lastIndexOf(":");
110
+ const ref = colon > slash ? pinned.slice(0, colon) : pinned;
111
+ if (!ref.startsWith("."))
112
+ return ref;
113
+ // Relative: resolve against the directory the bundle itself lives in.
114
+ const segments = bundleRef.split("/").slice(0, -1);
115
+ for (const segment of ref.split("/")) {
116
+ if (segment === "..")
117
+ segments.pop();
118
+ else if (segment !== "." && segment !== "")
119
+ segments.push(segment);
120
+ }
121
+ return segments.join("/");
122
+ }
61
123
  //# sourceMappingURL=catalog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/catalog.ts"],"names":[],"mappings":"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"}
@@ -67,3 +67,65 @@ export function timeAgo(iso: string): string {
67
67
  export function vscodeUrl(extension: string | null, ref: string): string | null {
68
68
  return extension ? `vscode://${extension}/open?repo=${encodeURIComponent(ref)}` : null;
69
69
  }
70
+
71
+ /**
72
+ * Alias charset the extension's `/add-registry` handler accepts. It has to be
73
+ * a TOML bare key and safe as a CLI argument, so it refuses a link rather
74
+ * than escaping one. Mirrored here so a config that cannot produce a working
75
+ * link renders no button at all, instead of one that silently does nothing.
76
+ */
77
+ const REGISTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
78
+
79
+ /**
80
+ * `vscode://<publisher.extension>/add-registry?index=<url>&alias=<name>` — the
81
+ * one-click counterpart of the `grim config registry add` line beside it.
82
+ *
83
+ * Null unless the link would actually work: the handler takes https only (an
84
+ * index locator is fetched with whatever credentials the user configures for
85
+ * it), refuses embedded credentials, and caps the URL at 2048 characters.
86
+ * Checking here keeps a broken button off the page rather than putting the
87
+ * failure in the user's hands.
88
+ */
89
+ export function addRegistryUrl(
90
+ extension: string | null,
91
+ registry: { alias: string; index: string } | null,
92
+ ): string | null {
93
+ if (!extension || !registry || !REGISTRY_ALIAS.test(registry.alias)) return null;
94
+ let url: URL;
95
+ try {
96
+ url = new URL(registry.index);
97
+ } catch {
98
+ return null;
99
+ }
100
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return null;
101
+ if (url.href.length > 2048) return null;
102
+ const query = new URLSearchParams({ index: url.href, alias: registry.alias });
103
+ return `vscode://${extension}/add-registry?${query.toString()}`;
104
+ }
105
+
106
+ /**
107
+ * A bundle member's `id` resolved to the reference the index stores.
108
+ *
109
+ * grim writes members relative to the bundle itself — `../skills/grim-usage:0`
110
+ * — so a member published alongside its bundle stays correct wherever the
111
+ * whole namespace is mirrored. The catalog keys on absolute, untagged refs,
112
+ * which is what this returns; an id that is already absolute passes through
113
+ * with only its tag or digest removed.
114
+ */
115
+ export function resolveMemberRef(bundleRef: string, id: string): string {
116
+ const pinned = id.split("@")[0] ?? id;
117
+ // A tag is a colon *after* the last slash — `localhost:5000/x` is a host,
118
+ // not a tag.
119
+ const slash = pinned.lastIndexOf("/");
120
+ const colon = pinned.lastIndexOf(":");
121
+ const ref = colon > slash ? pinned.slice(0, colon) : pinned;
122
+ if (!ref.startsWith(".")) return ref;
123
+
124
+ // Relative: resolve against the directory the bundle itself lives in.
125
+ const segments = bundleRef.split("/").slice(0, -1);
126
+ for (const segment of ref.split("/")) {
127
+ if (segment === "..") segments.pop();
128
+ else if (segment !== "." && segment !== "") segments.push(segment);
129
+ }
130
+ return segments.join("/");
131
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Light and dark code themes.
3
+ *
4
+ * Shiki is what Astro already ships — the same highlighter VS Code renders
5
+ * with — so nothing here adds a library; this only stops the two consumers
6
+ * drifting onto different themes. The markdown pipeline reads it through
7
+ * `astro.config`'s `shikiConfig`, and the `<Code>` component on the package
8
+ * page takes it as a prop.
9
+ *
10
+ * A *pair*, not one theme: with a single theme a block stays dark on a light
11
+ * page. Given two, Shiki writes the light colour as an inline style and the
12
+ * dark one as a `--shiki-dark` custom property on the same token, and
13
+ * `Base.astro` swaps between them with the rest of the palette.
14
+ *
15
+ * GitHub's pair, deliberately boring: Shiki's best-tested themes, and neutral
16
+ * enough to sit on this page without fighting the accent.
17
+ */
18
+ export declare const SHIKI_THEMES: {
19
+ readonly light: "github-light";
20
+ readonly dark: "github-dark";
21
+ };
22
+ //# sourceMappingURL=code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/code.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY;;;CAA0D,CAAC"}
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ /**
4
+ * Light and dark code themes.
5
+ *
6
+ * Shiki is what Astro already ships — the same highlighter VS Code renders
7
+ * with — so nothing here adds a library; this only stops the two consumers
8
+ * drifting onto different themes. The markdown pipeline reads it through
9
+ * `astro.config`'s `shikiConfig`, and the `<Code>` component on the package
10
+ * page takes it as a prop.
11
+ *
12
+ * A *pair*, not one theme: with a single theme a block stays dark on a light
13
+ * page. Given two, Shiki writes the light colour as an inline style and the
14
+ * dark one as a `--shiki-dark` custom property on the same token, and
15
+ * `Base.astro` swaps between them with the rest of the palette.
16
+ *
17
+ * GitHub's pair, deliberately boring: Shiki's best-tested themes, and neutral
18
+ * enough to sit on this page without fighting the accent.
19
+ */
20
+ export const SHIKI_THEMES = { light: "github-light", dark: "github-dark" };
21
+ //# sourceMappingURL=code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code.js","sourceRoot":"","sources":["../../../../src/renderer/astro/lib/code.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAW,CAAC"}
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+
4
+ /**
5
+ * Light and dark code themes.
6
+ *
7
+ * Shiki is what Astro already ships — the same highlighter VS Code renders
8
+ * with — so nothing here adds a library; this only stops the two consumers
9
+ * drifting onto different themes. The markdown pipeline reads it through
10
+ * `astro.config`'s `shikiConfig`, and the `<Code>` component on the package
11
+ * page takes it as a prop.
12
+ *
13
+ * A *pair*, not one theme: with a single theme a block stays dark on a light
14
+ * page. Given two, Shiki writes the light colour as an inline style and the
15
+ * dark one as a `--shiki-dark` custom property on the same token, and
16
+ * `Base.astro` swaps between them with the rest of the palette.
17
+ *
18
+ * GitHub's pair, deliberately boring: Shiki's best-tested themes, and neutral
19
+ * enough to sit on this page without fighting the accent.
20
+ */
21
+ export const SHIKI_THEMES = { light: "github-light", dark: "github-dark" } as const;
@@ -1,29 +1,75 @@
1
1
  ---
2
2
  import Base from "../layouts/Base.astro";
3
3
  import Catalog from "../components/Catalog.tsx";
4
- import CopyButton from "../components/CopyButton.astro";
4
+ import CommandField from "../components/CommandField.astro";
5
+ import PickerMenu from "../components/PickerMenu.astro";
6
+ import { BrandMark } from "../components/BrandMark.tsx";
7
+ import { FolderRoot, Globe } from "lucide-preact";
8
+ import { addRegistryUrl } from "../lib/catalog";
5
9
  import { data } from "../lib/data";
10
+ import {
11
+ mdiApple,
12
+ mdiConsole,
13
+ mdiMicrosoftVisualStudioCode,
14
+ mdiMicrosoftWindows,
15
+ mdiPenguin,
16
+ } from "@mdi/js";
6
17
 
7
18
  const { config, packages } = data;
8
19
 
9
- // The "add this index" ergonomic (`helm repo add`): the command a visitor
10
- // needs to point their own grim at *this* index, plus the equivalent
11
- // hand-edit. Absent `registry` config means the index has no public URL to
12
- // hand out, so the block is omitted rather than guessed.
13
- const registryRows = config.registry
20
+ // One-click counterpart of the `grim config registry add` line: the VS Code
21
+ // extension registers a URI handler that writes the registry after a modal
22
+ // confirmation. Null whenever the link would not work see `addRegistryUrl`.
23
+ const addRegistry = addRegistryUrl(config.vscodeExtension, config.registry);
24
+
25
+ // The extension's own marketplace page — the one thing on this page that
26
+ // helps a visitor who does not have it installed yet.
27
+ const marketplace = config.vscodeExtension
28
+ ? `https://marketplace.visualstudio.com/items?itemName=${config.vscodeExtension}`
29
+ : null;
30
+
31
+ // The "add this index" ergonomic (`helm repo add`): the one command a
32
+ // visitor needs to point their own grim at *this* index. Absent `registry`
33
+ // config means the index has no public URL to hand out, so the block is
34
+ // omitted rather than guessed.
35
+ const addCommand = config.registry
36
+ ? `grim config registry add ${config.registry.alias} --index ${config.registry.index}`
37
+ : null;
38
+
39
+ // One installer row usually covers several platforms ("Linux / macOS"), and
40
+ // the toggle reads better as the platforms themselves than as the config's
41
+ // row labels — so a row expands into one button per platform it names, all
42
+ // pointing at the same command. A row naming none (a custom label) keeps a
43
+ // single generic button, so an unrecognized platform is never dropped.
44
+ //
45
+ // `mdiPenguin`, not `mdiLinux`: Tux's own silhouette is a solid mass that
46
+ // reads as an indistinct blob at 16px, where the plainer penguin keeps its
47
+ // outline. Still a brand mark from the same set, so it sits beside Apple
48
+ // and Windows without a weight change.
49
+ const PLATFORMS = [
50
+ { match: /linux/i, name: "Linux", path: mdiPenguin },
51
+ { match: /mac|darwin|osx|apple/i, name: "macOS", path: mdiApple },
52
+ { match: /win/i, name: "Windows", path: mdiMicrosoftWindows },
53
+ ];
54
+ const installChoices = config.install.flatMap((row) => {
55
+ const hits = PLATFORMS.filter((p) => p.match.test(row.os));
56
+ return hits.length > 0
57
+ ? hits.map((p) => ({ name: p.name, path: p.path, command: row.command }))
58
+ : [{ name: row.os, path: mdiConsole, command: row.command }];
59
+ });
60
+
61
+ // Registry scope, the same two-way choice the package cards offer and drawn
62
+ // with the same two Lucide glyphs. Global leads: someone arriving from an
63
+ // index website wants this index available everywhere, not wired into
64
+ // whichever directory happens to be open — and it is also the scope that
65
+ // works with no project at all.
66
+ // `--global` leads rather than trails: it is a top-level flag, and appended
67
+ // after a long `--index <url>` it fell off the end of the line, so switching
68
+ // scope looked like it changed nothing at all.
69
+ const scopeChoices = addCommand
14
70
  ? [
15
- {
16
- os: "add index",
17
- command: `grim config registry add ${config.registry.alias} --index ${config.registry.index}`,
18
- block: false,
19
- label: "Copy the registry add command",
20
- },
21
- {
22
- os: "grimoire.toml",
23
- command: `[[registries]]\nalias = "${config.registry.alias}"\nindex = "${config.registry.index}"`,
24
- block: true,
25
- label: "Copy the registries TOML entry",
26
- },
71
+ { name: "Global", command: `grim --global ${addCommand.slice("grim ".length)}`, Icon: Globe },
72
+ { name: "Project", command: addCommand, Icon: FolderRoot },
27
73
  ]
28
74
  : [];
29
75
  ---
@@ -37,33 +83,95 @@ const registryRows = config.registry
37
83
  config.installDocsUrl && (
38
84
  <Fragment>
39
85
  {" "}
40
- Copy an <code>add</code> command below — or{" "}
41
- <a href={config.installDocsUrl}>install grim</a> first.
86
+ <a href={config.installDocsUrl}>Install grim</a>, add this index,
87
+ then <code>grim add</code> anything below.
42
88
  </Fragment>
43
89
  )
44
90
  }
45
91
  </p>
92
+ {/* Beside the description, not under it: getting set up is a sidebar to
93
+ reading what this index is. Two labelled rows of one shape — the
94
+ command, then the ways to act on it, inside a single border. */}
46
95
  {
47
- (config.install.length > 0 || registryRows.length > 0) && (
48
- <div class="install">
49
- {config.install.map((row) => (
50
- <div class="install-row">
51
- <span class="os">{row.os}</span>
52
- <code>{row.command}</code>
53
- <CopyButton value={row.command} label={`Copy the ${row.os} install command`} />
54
- </div>
55
- ))}
56
- {registryRows.map((row) => (
57
- <div class="install-row">
58
- <span class="os">{row.os}</span>
59
- <code class:list={[{ block: row.block }]}>{row.command}</code>
60
- <CopyButton value={row.command} label={row.label} />
61
- </div>
62
- ))}
96
+ (installChoices.length > 0 || addCommand || addRegistry) && (
97
+ <div class="hero-side">
98
+ {installChoices.length > 0 && (
99
+ <Fragment>
100
+ <span class="side-label">install grim</span>
101
+ <div class="cmd-bar" data-os-switch data-os-detect data-os-noun="install command">
102
+ {installChoices.length > 1 && (
103
+ <PickerMenu
104
+ choices={installChoices}
105
+ title="Platform"
106
+ label="Choose your platform"
107
+ />
108
+ )}
109
+ <CommandField
110
+ value={installChoices[0].command}
111
+ name={`${installChoices[0].name} install command`}
112
+ label="Copy the install command"
113
+ />
114
+ {marketplace && (
115
+ <a
116
+ class="seg brand-link"
117
+ href={marketplace}
118
+ target="_blank"
119
+ rel="noopener noreferrer"
120
+ title="Get the VS Code extension"
121
+ aria-label="Get the VS Code extension from the marketplace"
122
+ >
123
+ <BrandMark path={mdiMicrosoftVisualStudioCode} />
124
+ </a>
125
+ )}
126
+ </div>
127
+ </Fragment>
128
+ )}
129
+ {(addCommand || addRegistry) && (
130
+ <Fragment>
131
+ <span class="side-label">add registry</span>
132
+ <div class="cmd-bar" data-os-switch data-os-noun="registry add command">
133
+ {scopeChoices.length > 0 && (
134
+ <PickerMenu choices={scopeChoices} title="Scope" label="Choose the scope" />
135
+ )}
136
+ {scopeChoices.length > 0 && (
137
+ <CommandField
138
+ value={scopeChoices[0].command}
139
+ name={`${scopeChoices[0].name} registry add command`}
140
+ label="Copy the registry add command"
141
+ />
142
+ )}
143
+ {addRegistry && (
144
+ <a
145
+ class="seg brand-link"
146
+ href={addRegistry}
147
+ title="Add this index via VS Code"
148
+ aria-label="Add this index via VS Code"
149
+ >
150
+ <BrandMark path={mdiMicrosoftVisualStudioCode} />
151
+ </a>
152
+ )}
153
+ </div>
154
+ </Fragment>
155
+ )}
63
156
  </div>
64
157
  )
65
158
  }
66
159
  </div>
67
160
 
161
+ {/* Right after the bar, on purpose: the layout's picker script sits at the
162
+ end of the document, past the whole catalog, and the browser can paint
163
+ before it runs — long enough to show a Linux command to someone on
164
+ Windows. Here it lands in the same parse as the hero. A platform the
165
+ bar does not offer leaves the configured first choice alone. */}
166
+ <script is:inline>
167
+ for (const bar of document.querySelectorAll("[data-os-detect]")) {
168
+ const host = window.__grimHostOs();
169
+ const match = [...bar.querySelectorAll("[data-os-pick]")].find(
170
+ (p) => p.dataset.osName === host,
171
+ );
172
+ if (match) window.__grimPick(bar, match);
173
+ }
174
+ </script>
175
+
68
176
  <Catalog client:load packages={packages} vscodeExtension={config.vscodeExtension} />
69
177
  </Base>