@grove-dev/astro 0.6.1 → 0.8.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 (111) hide show
  1. package/README.md +77 -5
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +22 -33
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/display.d.ts +1 -1
  7. package/dist/lib/display.js +1 -1
  8. package/dist/lib/format.d.ts +1 -1
  9. package/dist/lib/format.js +1 -1
  10. package/dist/lib/icon-kinds.d.ts +1 -1
  11. package/dist/lib/icon-kinds.js +43 -43
  12. package/dist/lib/icon-registry.d.ts +2 -2
  13. package/dist/lib/icon-registry.js +23 -23
  14. package/dist/lib/index.d.ts +9 -9
  15. package/dist/lib/index.d.ts.map +1 -1
  16. package/dist/lib/index.js +9 -9
  17. package/dist/lib/index.js.map +1 -1
  18. package/dist/lib/lenses.d.ts +1 -1
  19. package/dist/lib/lenses.js +1 -1
  20. package/dist/lib/packaged-icons.d.ts +1 -1
  21. package/dist/lib/packaged-icons.js +4 -4
  22. package/dist/lib/repo.d.ts +1 -1
  23. package/dist/lib/repo.js +1 -1
  24. package/dist/lib/search.d.ts +1 -1
  25. package/dist/lib/search.js +1 -1
  26. package/dist/lib/taxonomy-counts.d.ts +1 -1
  27. package/dist/lib/taxonomy-counts.js +1 -1
  28. package/package.json +7 -17
  29. package/src/index.ts +23 -38
  30. package/src/lib/display.ts +1 -1
  31. package/src/lib/format.ts +1 -1
  32. package/src/lib/icon-kinds.ts +44 -44
  33. package/src/lib/icon-registry.ts +25 -25
  34. package/src/lib/index.ts +9 -9
  35. package/src/lib/lenses.ts +1 -1
  36. package/src/lib/packaged-icons.ts +8 -8
  37. package/src/lib/repo.ts +1 -1
  38. package/src/lib/search.ts +1 -1
  39. package/src/lib/taxonomy-counts.ts +1 -1
  40. package/src/server/collections.ts +53 -42
  41. package/src/server/contrast.ts +14 -9
  42. package/src/server/directory.ts +22 -17
  43. package/src/server/index.ts +3 -2
  44. package/src/server/models.ts +404 -213
  45. package/src/server/seo.ts +34 -31
  46. package/src/styles.css +235 -91
  47. package/dist/layouts/index.d.ts +0 -25
  48. package/dist/layouts/index.d.ts.map +0 -1
  49. package/dist/layouts/index.js +0 -25
  50. package/dist/layouts/index.js.map +0 -1
  51. package/dist/ui/button.d.ts +0 -53
  52. package/dist/ui/button.d.ts.map +0 -1
  53. package/dist/ui/button.js +0 -90
  54. package/dist/ui/button.js.map +0 -1
  55. package/src/components/CardGrid.astro +0 -41
  56. package/src/components/CardIcon.astro +0 -67
  57. package/src/components/CategoryGrid.astro +0 -114
  58. package/src/components/CollectionCard.astro +0 -56
  59. package/src/components/CollectionIndex.astro +0 -102
  60. package/src/components/CollectionPage.astro +0 -128
  61. package/src/components/CollectionRow.astro +0 -81
  62. package/src/components/CollectionTeaser.astro +0 -47
  63. package/src/components/ContributorsGrid.astro +0 -143
  64. package/src/components/DirectoryIndexClient.astro +0 -510
  65. package/src/components/EditorialSummary.astro +0 -58
  66. package/src/components/FilterGroupMenu.astro +0 -69
  67. package/src/components/FilterOptions.astro +0 -49
  68. package/src/components/FinalCta.astro +0 -43
  69. package/src/components/Hero.astro +0 -243
  70. package/src/components/Icon.astro +0 -191
  71. package/src/components/IndexRow.astro +0 -37
  72. package/src/components/LanguageBreakdown.astro +0 -67
  73. package/src/components/MarkdownBody.astro +0 -37
  74. package/src/components/OriginalCollection.astro +0 -117
  75. package/src/components/Pagination.astro +0 -50
  76. package/src/components/PoweredBy.astro +0 -60
  77. package/src/components/ProjectCard.astro +0 -321
  78. package/src/components/RecordHeader.astro +0 -221
  79. package/src/components/RecordSection.astro +0 -128
  80. package/src/components/RecordSidebar.astro +0 -387
  81. package/src/components/RefinePanel.astro +0 -168
  82. package/src/components/SmartLensTabs.astro +0 -81
  83. package/src/components/StackGrid.astro +0 -151
  84. package/src/components/StackPlatformChips.astro +0 -73
  85. package/src/components/SubmissionClient.astro +0 -254
  86. package/src/components/TableOfContents.astro +0 -140
  87. package/src/components/WhyThisExists.astro +0 -116
  88. package/src/layouts/BaseLayout.astro +0 -415
  89. package/src/layouts/Container.astro +0 -18
  90. package/src/layouts/Footer.astro +0 -231
  91. package/src/layouts/Header.astro +0 -214
  92. package/src/layouts/SectionHeader.astro +0 -43
  93. package/src/layouts/Seo.astro +0 -171
  94. package/src/layouts/ThemeToggle.astro +0 -118
  95. package/src/layouts/astro-shim.d.ts +0 -29
  96. package/src/layouts/index.ts +0 -24
  97. package/src/server/collections.test.ts +0 -212
  98. package/src/server/contrast.test.ts +0 -82
  99. package/src/server/directory.test.ts +0 -177
  100. package/src/server/github-repo.test.ts +0 -88
  101. package/src/server/models-home.test.ts +0 -101
  102. package/src/server/models.test.ts +0 -135
  103. package/src/server/seo.test.ts +0 -123
  104. package/src/ui/Badge.astro +0 -38
  105. package/src/ui/Button.astro +0 -42
  106. package/src/ui/EmptyState.astro +0 -38
  107. package/src/ui/FilterDrawer.astro +0 -124
  108. package/src/ui/PageHeader.astro +0 -39
  109. package/src/ui/SearchField.astro +0 -106
  110. package/src/ui/button.test.ts +0 -54
  111. package/src/ui/button.ts +0 -107
@@ -1,10 +1,10 @@
1
- import type { Collection, CollectionEntry } from "@grove-dev/core";
2
- import { collectionSchema, findRelated, runCollection } from "@grove-dev/core";
3
- import { absoluteUrl, ogPath, type PageSeo, seoDescription, seoTitle } from "./seo.js";
1
+ import type { Collection, CollectionEntry } from '@grove-dev/core';
2
+ import { collectionSchema, findRelated, runCollection } from '@grove-dev/core';
3
+ import { absoluteUrl, ogPath, type PageSeo, seoDescription, seoTitle } from './seo.js';
4
4
 
5
5
  // Collection YAML loading lives in @grove-dev/core (it also feeds the
6
6
  // sitemap and OG-image pipelines there); re-exported for page code.
7
- export { loadCollections } from "@grove-dev/core";
7
+ export { loadCollections } from '@grove-dev/core';
8
8
 
9
9
  interface RouteHint {
10
10
  routeSlug?: string;
@@ -65,19 +65,18 @@ export function recordsToCollectionEntries(
65
65
  records: RawRecord[],
66
66
  site: RouteHint,
67
67
  ): CollectionEntry[] {
68
- const routeSlug = site.routeSlug ?? site.blueprintConfig?.routeSlug ?? "projects";
68
+ const routeSlug = site.routeSlug ?? site.blueprintConfig?.routeSlug ?? 'projects';
69
69
  const out: CollectionEntry[] = [];
70
70
  for (const r of records) {
71
- if (r.visibility === "hide") continue;
71
+ if (r.visibility === 'hide') continue;
72
72
  if (!r.slug) continue;
73
- const categories = [
74
- ...(r.category ? [r.category] : []),
75
- ...(r.tags ?? []),
76
- ].filter((value, index, self) => self.indexOf(value) === index);
73
+ const categories = [...(r.category ? [r.category] : []), ...(r.tags ?? [])].filter(
74
+ (value, index, self) => self.indexOf(value) === index,
75
+ );
77
76
  out.push({
78
77
  slug: r.slug,
79
78
  title: r.name ?? r.title ?? r.slug,
80
- description: r.description ?? "",
79
+ description: r.description ?? '',
81
80
  url: `/${routeSlug}/${r.slug}/`,
82
81
  repoHref: r.repoUrl ?? r.links?.github,
83
82
  homepageHref: r.links?.website,
@@ -89,11 +88,7 @@ export function recordsToCollectionEntries(
89
88
  // If only `license` (singular, GitHub-synced) is present, mirror
90
89
  // it into the array so the filter still matches it.
91
90
  licenses:
92
- r.licenses && r.licenses.length > 0
93
- ? r.licenses
94
- : r.license
95
- ? [r.license]
96
- : undefined,
91
+ r.licenses && r.licenses.length > 0 ? r.licenses : r.license ? [r.license] : undefined,
97
92
  status: r.visibility,
98
93
  stars: r.stars ?? r.github?.repository?.stargazers_count,
99
94
  forks: r.forks ?? r.github?.repository?.forks_count,
@@ -118,7 +113,7 @@ export interface CollectionPageModel {
118
113
  slug: string;
119
114
  title: string;
120
115
  description: string;
121
- kind: "curated" | "generated";
116
+ kind: 'curated' | 'generated';
122
117
  selectionNote?: string;
123
118
  introduction?: string;
124
119
  };
@@ -140,7 +135,7 @@ export interface CollectionIndexModel {
140
135
  slug: string;
141
136
  title: string;
142
137
  description: string;
143
- kind: "curated" | "generated";
138
+ kind: 'curated' | 'generated';
144
139
  count: number;
145
140
  url: string;
146
141
  }>;
@@ -151,14 +146,19 @@ export interface CollectionIndexModel {
151
146
 
152
147
  export interface CollectionTeaserModel {
153
148
  total: number;
154
- collections: CollectionIndexModel["collections"];
149
+ collections: CollectionIndexModel['collections'];
155
150
  }
156
151
 
157
152
  export function getCollectionPageModel(
158
153
  collection: Collection,
159
154
  entries: CollectionEntry[],
160
155
  allCollections: Collection[],
161
- site?: { name?: string; url?: string; siteUrl?: string; blueprintConfig?: { labelPlural?: string } },
156
+ site?: {
157
+ name?: string;
158
+ url?: string;
159
+ siteUrl?: string;
160
+ blueprintConfig?: { labelPlural?: string };
161
+ },
162
162
  ): CollectionPageModel {
163
163
  const result = runCollection(collection, entries);
164
164
  const related = findRelated(collection, allCollections, 4).map((c) => ({
@@ -166,9 +166,9 @@ export function getCollectionPageModel(
166
166
  title: c.title,
167
167
  url: `/collections/${c.slug}/`,
168
168
  }));
169
- const siteUrl = (site?.siteUrl ?? site?.url ?? "https://example.com").replace(/\/$/, "");
170
- const siteName = site?.name ?? "";
171
- const plural = site?.blueprintConfig?.labelPlural ?? "items";
169
+ const siteUrl = (site?.siteUrl ?? site?.url ?? 'https://example.com').replace(/\/$/, '');
170
+ const siteName = site?.name ?? '';
171
+ const plural = site?.blueprintConfig?.labelPlural ?? 'items';
172
172
  const pageUrl = absoluteUrl(siteUrl, `collections/${collection.slug}/`);
173
173
  // CollectionPage + ItemList + BreadcrumbList. The ItemList is capped
174
174
  // at 50 entries to keep the payload bounded; search engines don't
@@ -178,13 +178,13 @@ export function getCollectionPageModel(
178
178
  name: collection.seo?.title ?? collection.title,
179
179
  description: collection.seo?.description ?? collection.description,
180
180
  items: result.entries.slice(0, 50).map((entry) => ({
181
- url: entry.url.startsWith("http") ? entry.url : absoluteUrl(siteUrl, entry.url),
181
+ url: entry.url.startsWith('http') ? entry.url : absoluteUrl(siteUrl, entry.url),
182
182
  name: entry.title,
183
183
  ...(entry.description ? { description: entry.description } : {}),
184
184
  })),
185
185
  crumbs: [
186
- { url: `${siteUrl}/`, name: "Home" },
187
- { url: absoluteUrl(siteUrl, "collections/"), name: "Collections" },
186
+ { url: `${siteUrl}/`, name: 'Home' },
187
+ { url: absoluteUrl(siteUrl, 'collections/'), name: 'Collections' },
188
188
  { url: pageUrl, name: collection.title },
189
189
  ],
190
190
  });
@@ -195,11 +195,8 @@ export function getCollectionPageModel(
195
195
  title:
196
196
  collection.seo?.title ??
197
197
  seoTitle(`${collection.title} — ${result.entries.length} ${plural}`, siteName),
198
- description: seoDescription(
199
- collection.seo?.description,
200
- collection.description,
201
- ),
202
- image: ogPath("collection", collection.slug),
198
+ description: seoDescription(collection.seo?.description, collection.description),
199
+ image: ogPath('collection', collection.slug),
203
200
  ...(siteName ? { imageAlt: `${collection.title} — ${siteName}` } : {}),
204
201
  jsonLd: jsonLd as unknown as Record<string, unknown>[],
205
202
  noindex: collection.seo?.index === false,
@@ -225,7 +222,12 @@ export function getCollectionPageModel(
225
222
  export function getCollectionIndexModel(
226
223
  collections: Collection[],
227
224
  entries: CollectionEntry[],
228
- site?: { name?: string; url?: string; siteUrl?: string; blueprintConfig?: { labelPlural?: string } },
225
+ site?: {
226
+ name?: string;
227
+ url?: string;
228
+ siteUrl?: string;
229
+ blueprintConfig?: { labelPlural?: string };
230
+ },
229
231
  ): CollectionIndexModel {
230
232
  const rows = collections.map((c) => {
231
233
  const result = runCollection(c, entries);
@@ -240,20 +242,20 @@ export function getCollectionIndexModel(
240
242
  });
241
243
  let seo: PageSeo | undefined;
242
244
  if (site) {
243
- const siteUrl = (site.siteUrl ?? site.url ?? "https://example.com").replace(/\/$/, "");
244
- const siteName = site.name ?? "";
245
- const plural = site.blueprintConfig?.labelPlural ?? "items";
246
- const title = seoTitle("Collections", siteName);
245
+ const siteUrl = (site.siteUrl ?? site.url ?? 'https://example.com').replace(/\/$/, '');
246
+ const siteName = site.name ?? '';
247
+ const plural = site.blueprintConfig?.labelPlural ?? 'items';
248
+ const title = seoTitle('Collections', siteName);
247
249
  const description = seoDescription(
248
250
  undefined,
249
- `${rows.length} curated and generated collections of ${plural} on ${siteName || "this site"} — hand-picked lists kept in sync with the source files.`,
251
+ `${rows.length} curated and generated collections of ${plural} on ${siteName || 'this site'} — hand-picked lists kept in sync with the source files.`,
250
252
  );
251
253
  seo = {
252
254
  title,
253
255
  description,
254
- image: ogPath("default"),
256
+ image: ogPath('default'),
255
257
  jsonLd: collectionSchema({
256
- url: absoluteUrl(siteUrl, "collections/"),
258
+ url: absoluteUrl(siteUrl, 'collections/'),
257
259
  name: title,
258
260
  description,
259
261
  items: rows.map((row) => ({
@@ -262,8 +264,8 @@ export function getCollectionIndexModel(
262
264
  ...(row.description ? { description: row.description } : {}),
263
265
  })),
264
266
  crumbs: [
265
- { url: `${siteUrl}/`, name: "Home" },
266
- { url: absoluteUrl(siteUrl, "collections/"), name: "Collections" },
267
+ { url: `${siteUrl}/`, name: 'Home' },
268
+ { url: absoluteUrl(siteUrl, 'collections/'), name: 'Collections' },
267
269
  ],
268
270
  }) as unknown as Record<string, unknown>[],
269
271
  };
@@ -300,7 +302,16 @@ export function getCollectionTeaserModel(
300
302
  * label without re-loading the collection YAML.
301
303
  */
302
304
  export function findCollectionsFor(
303
- target: { slug?: string; stack?: string; stacks?: string[]; platforms?: string[]; licenses?: string[]; category?: string; visibility?: string; status?: string },
305
+ target: {
306
+ slug?: string;
307
+ stack?: string;
308
+ stacks?: string[];
309
+ platforms?: string[];
310
+ licenses?: string[];
311
+ category?: string;
312
+ visibility?: string;
313
+ status?: string;
314
+ },
304
315
  collections: Collection[],
305
316
  entries: CollectionEntry[],
306
317
  ): { slug: string; title: string; url: string }[] {
@@ -9,16 +9,22 @@
9
9
  * values are plain hex.
10
10
  */
11
11
 
12
- const INK_950 = "#0a0a0a";
13
- const WHITE = "#ffffff";
12
+ const INK_950 = '#0a0a0a';
13
+ const WHITE = '#ffffff';
14
14
  const AA_NORMAL = 4.5;
15
15
 
16
16
  export type Rgb = [number, number, number];
17
17
 
18
18
  /** Parse #rgb / #rrggbb (also tolerates #rrggbbaa, alpha ignored). */
19
19
  export function hexToRgb(hex: string): Rgb | null {
20
- const raw = hex.replace("#", "");
21
- const full = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
20
+ const raw = hex.replace('#', '');
21
+ const full =
22
+ raw.length === 3
23
+ ? raw
24
+ .split('')
25
+ .map((c) => c + c)
26
+ .join('')
27
+ : raw;
22
28
  if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(full)) return null;
23
29
  const int = Number.parseInt(full.slice(0, 6), 16);
24
30
  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
@@ -26,7 +32,7 @@ export function hexToRgb(hex: string): Rgb | null {
26
32
 
27
33
  export function rgbToHex([r, g, b]: Rgb): string {
28
34
  const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
29
- return `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, "0")).join("")}`;
35
+ return `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, '0')).join('')}`;
30
36
  }
31
37
 
32
38
  /** WCAG relative luminance (sRGB linearization, not the quick luma). */
@@ -104,14 +110,13 @@ export function derivePrimaryPalette(hex: string): PrimaryPalette | null {
104
110
  } else if (contrastRatio(hex, INK_950) >= AA_NORMAL) {
105
111
  solidForeground = INK_950;
106
112
  } else {
107
- solid = adjustUntilAA(hex, "#000000", WHITE);
113
+ solid = adjustUntilAA(hex, '#000000', WHITE);
108
114
  solidForeground = WHITE;
109
115
  }
110
116
 
111
117
  const lifted = rgbToHex(mix(rgb, hexToRgb(WHITE) as Rgb, 0.28));
112
- const dark = contrastRatio(lifted, INK_950) >= AA_NORMAL
113
- ? lifted
114
- : adjustUntilAA(lifted, WHITE, INK_950);
118
+ const dark =
119
+ contrastRatio(lifted, INK_950) >= AA_NORMAL ? lifted : adjustUntilAA(lifted, WHITE, INK_950);
115
120
 
116
121
  return { solid, solidForeground, dark, darkForeground: INK_950 };
117
122
  }
@@ -30,27 +30,28 @@
30
30
  * resolution error, which is the correct signal — a build that
31
31
  * silently renders an empty directory hides real config mistakes.
32
32
  */
33
+
34
+ import { existsSync, readFileSync } from 'node:fs';
35
+ import { dirname, resolve } from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
33
37
  import fullPayload from '@grove/generated/records.full.json';
34
38
  import indexPayload from '@grove/generated/records.index.json';
35
39
  import siteConfigPayload from '@grove/generated/site-config.json';
36
- import { existsSync, readFileSync } from 'node:fs';
37
- import { fileURLToPath } from 'node:url';
38
- import { dirname, resolve } from 'node:path';
39
- import { marked } from 'marked';
40
- import sanitizeHtml from 'sanitize-html';
41
- import { getSingletonHighlighter } from 'shiki';
42
- import { prettySlug } from '../lib/display.js';
43
- import { headingSlug, readContentFile, stripFrontmatter, uniqueSlug } from '@grove-dev/core';
44
40
  import type {
45
- ProjectRecord,
46
- ResourceRecord,
47
41
  EntityRecord,
48
- Resource,
49
- IndexRecord,
42
+ IndexEntityRecord,
50
43
  IndexProjectRecord,
44
+ IndexRecord,
51
45
  IndexResourceRecord,
52
- IndexEntityRecord,
46
+ ProjectRecord,
47
+ Resource,
48
+ ResourceRecord,
53
49
  } from '@grove-dev/core';
50
+ import { headingSlug, readContentFile, stripFrontmatter, uniqueSlug } from '@grove-dev/core';
51
+ import { marked } from 'marked';
52
+ import sanitizeHtml from 'sanitize-html';
53
+ import { getSingletonHighlighter } from 'shiki';
54
+ import { prettySlug } from '../lib/display.js';
54
55
 
55
56
  interface FullPayload {
56
57
  schemaVersion?: number;
@@ -676,10 +677,14 @@ const SUPPORTED_LANGS = [
676
677
  'properties',
677
678
  ];
678
679
  const globalAny = globalThis as GlobalWithShiki;
679
- const highlighter = await (globalAny[SHIKI_HIGHLIGHTER] ??= getSingletonHighlighter({
680
- themes: ['github-light', 'github-dark-default'],
681
- langs: SUPPORTED_LANGS,
682
- }));
680
+ const pendingHighlighter =
681
+ globalAny[SHIKI_HIGHLIGHTER] ??
682
+ getSingletonHighlighter({
683
+ themes: ['github-light', 'github-dark-default'],
684
+ langs: SUPPORTED_LANGS,
685
+ });
686
+ globalAny[SHIKI_HIGHLIGHTER] = pendingHighlighter;
687
+ const highlighter = await pendingHighlighter;
683
688
 
684
689
  // Release the previous engine's WASM heap on HMR. Without this the
685
690
  // old engine is unreachable but not disposed, which is the leak
@@ -1,5 +1,6 @@
1
- export * from './directory.js';
2
- export * from './models.js';
3
1
  export * from './collections.js';
2
+ export * from './contrast.js';
3
+ export * from './directory.js';
4
4
  export * from './github-repo.js';
5
+ export * from './models.js';
5
6
  export * from './seo.js';