@grimoire-rs/indexer 0.4.3 → 0.5.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 (61) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/NOTICE +30 -0
  3. package/README.md +76 -331
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +35 -4
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/config.d.ts +107 -7
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +181 -36
  10. package/dist/config.js.map +1 -1
  11. package/dist/renderer/astro/components/CardLogo.d.ts +5 -0
  12. package/dist/renderer/astro/components/CardLogo.js +58 -0
  13. package/dist/renderer/astro/components/CardLogo.tsx +96 -0
  14. package/dist/renderer/astro/components/Catalog.d.ts +14 -1
  15. package/dist/renderer/astro/components/Catalog.js +467 -108
  16. package/dist/renderer/astro/components/Catalog.tsx +756 -349
  17. package/dist/renderer/astro/components/CommandBar.astro +66 -0
  18. package/dist/renderer/astro/components/CopyButton.d.ts +7 -0
  19. package/dist/renderer/astro/components/CopyButton.js +28 -0
  20. package/dist/renderer/astro/components/CopyButton.tsx +56 -0
  21. package/dist/renderer/astro/components/KindMark.d.ts +69 -0
  22. package/dist/renderer/astro/components/KindMark.js +66 -0
  23. package/dist/renderer/astro/components/KindMark.tsx +141 -0
  24. package/dist/renderer/astro/components/PackageCard.d.ts +18 -0
  25. package/dist/renderer/astro/components/PackageCard.js +50 -0
  26. package/dist/renderer/astro/components/PackageCard.tsx +273 -0
  27. package/dist/renderer/astro/components/PackageRow.d.ts +10 -0
  28. package/dist/renderer/astro/components/PackageRow.js +32 -0
  29. package/dist/renderer/astro/components/PackageRow.tsx +126 -0
  30. package/dist/renderer/astro/components/PickerMenu.astro +5 -14
  31. package/dist/renderer/astro/components/SiteFooter.astro +64 -0
  32. package/dist/renderer/astro/components/SiteHeader.astro +74 -0
  33. package/dist/renderer/astro/components/VersionMenu.astro +2 -2
  34. package/dist/renderer/astro/layouts/Base.astro +860 -206
  35. package/dist/renderer/astro/lib/base.d.ts +25 -0
  36. package/dist/renderer/astro/lib/base.js +23 -0
  37. package/dist/renderer/astro/lib/base.ts +27 -0
  38. package/dist/renderer/astro/lib/catalog.d.ts +24 -0
  39. package/dist/renderer/astro/lib/catalog.js +36 -0
  40. package/dist/renderer/astro/lib/catalog.ts +37 -0
  41. package/dist/renderer/astro/lib/commands.d.ts +58 -0
  42. package/dist/renderer/astro/lib/commands.js +86 -0
  43. package/dist/renderer/astro/lib/commands.ts +117 -0
  44. package/dist/renderer/astro/lib/keywordRail.d.ts +44 -0
  45. package/dist/renderer/astro/lib/keywordRail.js +99 -0
  46. package/dist/renderer/astro/lib/keywordRail.ts +110 -0
  47. package/dist/renderer/astro/pages/index.astro +40 -87
  48. package/dist/renderer/astro/pages/p/[...slug].astro +340 -195
  49. package/dist/renderer/astro/styles/tokens.css +40 -5
  50. package/dist/renderer/index.d.ts +58 -0
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +547 -5
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/renderer/types.d.ts +9 -0
  55. package/dist/renderer/types.d.ts.map +1 -1
  56. package/package.json +9 -4
  57. package/templates/README.md +6 -0
  58. package/templates/ci/github-ratings.yml +5 -0
  59. package/templates/gitignore +4 -1
  60. package/templates/theme/README.md +38 -0
  61. package/templates/tsconfig.json +47 -0
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+
4
+ export interface KeywordChip {
5
+ keyword: string;
6
+ count: number;
7
+ }
8
+
9
+ /**
10
+ * The rail's chips, picked by SPLITTING POWER rather than raw frequency.
11
+ *
12
+ * A chip is worth a rail slot when clicking it meaningfully partitions what
13
+ * is on screen, so:
14
+ *
15
+ * - a near-ubiquitous keyword scores ~0 (clicking it barely narrows),
16
+ * - a tiny keyword scores low (it barely selects anything),
17
+ * - a keyword redundant with one already picked scores low (its packages are
18
+ * already reachable through that one).
19
+ *
20
+ * Greedy. Each round scores every unpicked keyword as
21
+ * `min(uncoveredCount, total - count)` — a tent over coverage, peaking near
22
+ * half the set, intersected with the marginal gain over what is already
23
+ * covered — and takes the best. Ties go to the higher total count, then
24
+ * alphabetically. When nothing scores above zero (a small or homogeneous
25
+ * catalog) the remaining slots are padded by plain frequency, so the rail is
26
+ * never emptier than it has to be.
27
+ *
28
+ * The input is deliberately narrow — only `keywords` is read — so the tests
29
+ * need no full `CatalogPackage` fixtures.
30
+ *
31
+ * Ported from `@ocx-sh/catalog`'s `src/theme/utils/keywordRail.ts`, which
32
+ * arrived at this after a frequency-ranked rail kept offering the tags every
33
+ * package already carries.
34
+ */
35
+ export function selectRailKeywords(
36
+ items: readonly { keywords?: readonly string[] }[],
37
+ limit: number,
38
+ ): KeywordChip[] {
39
+ const total = items.length;
40
+ const members = new Map<string, Set<number>>();
41
+ items.forEach((item, i) => {
42
+ for (const kw of item.keywords ?? []) {
43
+ let set = members.get(kw);
44
+ if (!set) members.set(kw, (set = new Set()));
45
+ set.add(i);
46
+ }
47
+ });
48
+
49
+ const picked: KeywordChip[] = [];
50
+ const pickedSet = new Set<string>();
51
+ const covered = new Set<number>();
52
+
53
+ while (picked.length < limit) {
54
+ let best: string | null = null;
55
+ let bestScore = 0;
56
+ let bestCount = 0;
57
+ for (const [kw, set] of members) {
58
+ if (pickedSet.has(kw)) continue;
59
+ let uncovered = 0;
60
+ for (const i of set) if (!covered.has(i)) uncovered++;
61
+ const score = Math.min(uncovered, total - set.size);
62
+ const wins =
63
+ score > bestScore ||
64
+ (score === bestScore &&
65
+ score > 0 &&
66
+ (set.size > bestCount ||
67
+ (set.size === bestCount && best !== null && kw < best)));
68
+ if (wins) {
69
+ best = kw;
70
+ bestScore = score;
71
+ bestCount = set.size;
72
+ }
73
+ }
74
+ if (best === null) break;
75
+ picked.push({ keyword: best, count: members.get(best)!.size });
76
+ pickedSet.add(best);
77
+ for (const i of members.get(best)!) covered.add(i);
78
+ }
79
+
80
+ // Frequency padding for the slots the greedy pass could not justify.
81
+ if (picked.length < limit) {
82
+ const rest = [...members]
83
+ .filter(([kw]) => !pickedSet.has(kw))
84
+ .map(([keyword, set]) => ({ keyword, count: set.size }))
85
+ .sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword));
86
+ picked.push(...rest.slice(0, limit - picked.length));
87
+ }
88
+
89
+ return picked;
90
+ }
91
+
92
+ /**
93
+ * Every keyword in `items`, most common first. Feeds the overflow menu, which
94
+ * lists what the rail had no room for.
95
+ *
96
+ * Scored over the same set the rail is, so a menu row is never a click to an
97
+ * empty catalog: a keyword no surviving package carries is simply not there.
98
+ */
99
+ export function keywordFrequency(
100
+ items: readonly { keywords?: readonly string[] }[],
101
+ ): KeywordChip[] {
102
+ const counts = new Map<string, number>();
103
+ for (const item of items) {
104
+ for (const kw of item.keywords ?? [])
105
+ counts.set(kw, (counts.get(kw) ?? 0) + 1);
106
+ }
107
+ return [...counts]
108
+ .map(([keyword, count]) => ({ keyword, count }))
109
+ .sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword));
110
+ }
@@ -1,19 +1,12 @@
1
1
  ---
2
2
  import Base from "../layouts/Base.astro";
3
3
  import Catalog from "../components/Catalog.tsx";
4
- import CommandField from "../components/CommandField.astro";
5
- import PickerMenu from "../components/PickerMenu.astro";
4
+ import CommandBar from "../components/CommandBar.astro";
6
5
  import { BrandMark } from "../components/BrandMark.tsx";
7
- import { FolderRoot, Globe } from "lucide-preact";
8
6
  import { addRegistryUrl } from "../lib/catalog";
7
+ import { installChoices, registryAddCommand, registryScopeChoices } from "../lib/commands";
9
8
  import { data } from "../lib/data";
10
- import {
11
- mdiApple,
12
- mdiConsole,
13
- mdiMicrosoftVisualStudioCode,
14
- mdiMicrosoftWindows,
15
- mdiPenguin,
16
- } from "@mdi/js";
9
+ import { mdiMicrosoftVisualStudioCode } from "@mdi/js";
17
10
 
18
11
  const { config, packages } = data;
19
12
 
@@ -28,50 +21,11 @@ const marketplace = config.vscodeExtension
28
21
  ? `https://marketplace.visualstudio.com/items?itemName=${config.vscodeExtension}`
29
22
  : null;
30
23
 
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
70
- ? [
71
- { name: "Global", command: `grim --global ${addCommand.slice("grim ".length)}`, Icon: Globe },
72
- { name: "Project", command: addCommand, Icon: FolderRoot },
73
- ]
74
- : [];
24
+ // All three derivations live in `lib/commands` rather than here, so a page an
25
+ // index adds under `theme/pages/` can draw these same bars see CommandBar.
26
+ const install = installChoices(config);
27
+ const addCommand = registryAddCommand(config);
28
+ const scopeChoices = registryScopeChoices(config);
75
29
  ---
76
30
 
77
31
  <Base title={config.brand} description={config.description}>
@@ -93,26 +47,22 @@ const scopeChoices = addCommand
93
47
  reading what this index is. Two labelled rows of one shape — the
94
48
  command, then the ways to act on it, inside a single border. */}
95
49
  {
96
- (installChoices.length > 0 || addCommand || addRegistry) && (
50
+ (install.length > 0 || addCommand || addRegistry) && (
97
51
  <div class="hero-side">
98
- {installChoices.length > 0 && (
52
+ {install.length > 0 && (
99
53
  <Fragment>
100
54
  <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
- />
55
+ <CommandBar
56
+ choices={install}
57
+ noun="install command"
58
+ copyLabel="Copy the install command"
59
+ pickerTitle="Platform"
60
+ pickerLabel="Choose your platform"
61
+ detect
62
+ >
114
63
  {marketplace && (
115
64
  <a
65
+ slot="action"
116
66
  class="seg brand-link"
117
67
  href={marketplace}
118
68
  target="_blank"
@@ -123,25 +73,22 @@ const scopeChoices = addCommand
123
73
  <BrandMark path={mdiMicrosoftVisualStudioCode} />
124
74
  </a>
125
75
  )}
126
- </div>
76
+ </CommandBar>
127
77
  </Fragment>
128
78
  )}
129
79
  {(addCommand || addRegistry) && (
130
80
  <Fragment>
131
81
  <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
- )}
82
+ <CommandBar
83
+ choices={scopeChoices}
84
+ noun="registry add command"
85
+ copyLabel="Copy the registry add command"
86
+ pickerTitle="Scope"
87
+ pickerLabel="Choose the scope"
88
+ >
143
89
  {addRegistry && (
144
90
  <a
91
+ slot="action"
145
92
  class="seg brand-link"
146
93
  href={addRegistry}
147
94
  title="Add this index via VS Code"
@@ -150,7 +97,7 @@ const scopeChoices = addCommand
150
97
  <BrandMark path={mdiMicrosoftVisualStudioCode} />
151
98
  </a>
152
99
  )}
153
- </div>
100
+ </CommandBar>
154
101
  </Fragment>
155
102
  )}
156
103
  </div>
@@ -158,11 +105,17 @@ const scopeChoices = addCommand
158
105
  }
159
106
  </div>
160
107
 
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. */}
108
+ {/* A deliberate duplicate of the layout's own preselect loop, kept for
109
+ one reason: that one sits at the end of the document, past the whole
110
+ catalog, and the browser can paint before it runs — long enough to
111
+ show a Linux command to someone on Windows. This lands in the same
112
+ parse as the hero.
113
+
114
+ The layout's copy is what makes `detect` work on a page under
115
+ `theme/pages/`, which has no script of its own; both runs pick the same
116
+ choice and `__grimPick` is idempotent, so running twice here costs a
117
+ few microseconds and nothing else. A platform the bar does not offer
118
+ leaves the configured first choice alone. */}
166
119
  <script is:inline>
167
120
  for (const bar of document.querySelectorAll("[data-os-detect]")) {
168
121
  const host = window.__grimHostOs();