@grove-dev/astro 0.1.5 → 0.2.5

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +4 -25
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +56 -22
  5. package/dist/index.js.map +1 -1
  6. package/dist/layouts/index.d.ts +25 -0
  7. package/dist/layouts/index.d.ts.map +1 -0
  8. package/dist/layouts/index.js +25 -0
  9. package/dist/layouts/index.js.map +1 -0
  10. package/dist/lib/display.d.ts +33 -0
  11. package/dist/lib/display.d.ts.map +1 -0
  12. package/dist/lib/display.js +104 -0
  13. package/dist/lib/display.js.map +1 -0
  14. package/dist/lib/format.d.ts +30 -0
  15. package/dist/lib/format.d.ts.map +1 -0
  16. package/dist/lib/format.js +73 -0
  17. package/dist/lib/format.js.map +1 -0
  18. package/dist/lib/index.d.ts +26 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/index.js +26 -0
  21. package/dist/lib/index.js.map +1 -0
  22. package/dist/lib/lenses.d.ts +96 -0
  23. package/dist/lib/lenses.d.ts.map +1 -0
  24. package/dist/lib/lenses.js +181 -0
  25. package/dist/lib/lenses.js.map +1 -0
  26. package/dist/lib/repo.d.ts +30 -0
  27. package/dist/lib/repo.d.ts.map +1 -0
  28. package/dist/lib/repo.js +44 -0
  29. package/dist/lib/repo.js.map +1 -0
  30. package/dist/lib/scores.d.ts +38 -0
  31. package/dist/lib/scores.d.ts.map +1 -0
  32. package/dist/lib/scores.js +83 -0
  33. package/dist/lib/scores.js.map +1 -0
  34. package/dist/lib/search.d.ts +199 -0
  35. package/dist/lib/search.d.ts.map +1 -0
  36. package/dist/lib/search.js +357 -0
  37. package/dist/lib/search.js.map +1 -0
  38. package/dist/lib/taxonomy-counts.d.ts +25 -0
  39. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  40. package/dist/lib/taxonomy-counts.js +41 -0
  41. package/dist/lib/taxonomy-counts.js.map +1 -0
  42. package/package.json +7 -7
  43. package/src/components/AppsIndexRow.astro +204 -0
  44. package/src/components/AppsPagination.astro +126 -0
  45. package/src/components/CategoryGrid.astro +86 -17
  46. package/src/components/ContributorsGrid.astro +149 -0
  47. package/src/components/DecisionRow.astro +89 -0
  48. package/src/components/ExploreByCategory.astro +67 -0
  49. package/src/components/ExploreByStack.astro +72 -0
  50. package/src/components/Hero.astro +246 -0
  51. package/src/components/Icon.astro +223 -0
  52. package/src/components/ItemCard.astro +306 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RecordSection.astro +88 -0
  56. package/src/components/RefinePanel.astro +197 -0
  57. package/src/components/ScoreBars.astro +92 -22
  58. package/src/components/SmartLensTabs.astro +71 -0
  59. package/src/components/StackGrid.astro +132 -0
  60. package/src/components/WhyThisExists.astro +116 -0
  61. package/src/index.ts +61 -14
  62. package/src/layouts/BaseLayout.astro +227 -15
  63. package/src/layouts/Container.astro +57 -0
  64. package/src/layouts/Footer.astro +235 -0
  65. package/src/layouts/Header.astro +159 -0
  66. package/src/layouts/SectionHeader.astro +58 -0
  67. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  68. package/src/layouts/ThemeToggle.astro +110 -0
  69. package/src/layouts/astro-shim.d.ts +29 -0
  70. package/src/layouts/index.ts +24 -0
  71. package/src/lib/display.ts +118 -0
  72. package/src/lib/format.ts +65 -0
  73. package/src/lib/index.ts +26 -0
  74. package/src/lib/lenses.ts +241 -0
  75. package/src/lib/repo.ts +44 -0
  76. package/src/lib/scores.ts +94 -0
  77. package/src/lib/search.ts +481 -0
  78. package/src/lib/taxonomy-counts.ts +60 -0
  79. package/src/styles.css +366 -638
  80. package/templates/default/astro.config.mjs +8 -6
  81. package/templates/default/content/pages/about.md +66 -0
  82. package/templates/default/content/pages/methodology.md +103 -0
  83. package/templates/default/data/generated/records.full.json +8 -0
  84. package/templates/default/data/generated/records.index.json +7 -0
  85. package/templates/default/data/generated/records.json +8 -0
  86. package/templates/default/data/generated/site-config.json +31 -0
  87. package/templates/default/data/records/.gitkeep +0 -0
  88. package/templates/default/grove.config.ts +36 -0
  89. package/templates/default/package.json +6 -5
  90. package/templates/default/public/llms-full.txt +14 -0
  91. package/templates/default/public/llms.txt +11 -0
  92. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  93. package/templates/default/src/components/layout/Footer.astro +16 -134
  94. package/templates/default/src/components/layout/Header.astro +25 -126
  95. package/templates/default/src/data/records.ts +193 -0
  96. package/templates/default/src/lib/markdown.ts +171 -0
  97. package/templates/default/src/pages/about.astro +95 -429
  98. package/templates/default/src/pages/index.astro +263 -61
  99. package/templates/default/src/pages/projects/[slug].astro +480 -0
  100. package/templates/default/src/pages/projects/index.astro +193 -0
  101. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  102. package/templates/default/src/pages/submit.astro +122 -411
  103. package/templates/default/src/styles/global.css +42 -68
  104. package/src/components/ActiveFilterChips.astro +0 -20
  105. package/src/components/DirectoryFilters.astro +0 -100
  106. package/src/components/DirectoryHero.astro +0 -32
  107. package/src/components/HealthBadge.astro +0 -12
  108. package/src/components/LensTabs.astro +0 -26
  109. package/src/components/MethodologyPanel.astro +0 -20
  110. package/src/components/Pagination.astro +0 -24
  111. package/src/components/ProjectDetail.astro +0 -166
  112. package/src/components/SubmitDraft.astro +0 -137
  113. package/templates/default/src/components/AppCard.astro +0 -248
  114. package/templates/default/src/components/AppSection.astro +0 -58
  115. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  116. package/templates/default/src/components/AppsPagination.astro +0 -147
  117. package/templates/default/src/components/CategoryGrid.astro +0 -80
  118. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  119. package/templates/default/src/components/DecisionRow.astro +0 -219
  120. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  121. package/templates/default/src/components/ExploreByStack.astro +0 -102
  122. package/templates/default/src/components/Hero.astro +0 -176
  123. package/templates/default/src/components/MinimalAbout.astro +0 -57
  124. package/templates/default/src/components/OriginalCollection.astro +0 -144
  125. package/templates/default/src/components/RefinePanel.astro +0 -272
  126. package/templates/default/src/components/ScoreBars.astro +0 -72
  127. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  128. package/templates/default/src/components/StackGrid.astro +0 -95
  129. package/templates/default/src/components/WhyThisExists.astro +0 -81
  130. package/templates/default/src/components/icons/Icon.astro +0 -148
  131. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  132. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  133. package/templates/default/src/data/apps.ts +0 -149
  134. package/templates/default/src/data/categories.ts +0 -27
  135. package/templates/default/src/data/config.ts +0 -28
  136. package/templates/default/src/data/contributors.ts +0 -96
  137. package/templates/default/src/data/stacks.ts +0 -47
  138. package/templates/default/src/data/stats.ts +0 -65
  139. package/templates/default/src/data/types.ts +0 -248
  140. package/templates/default/src/lib/apps-search.ts +0 -228
  141. package/templates/default/src/lib/format.ts +0 -36
  142. package/templates/default/src/lib/lenses.ts +0 -98
  143. package/templates/default/src/lib/repo.ts +0 -12
  144. package/templates/default/src/lib/scores.ts +0 -51
  145. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  146. package/templates/default/src/pages/apps/[slug].astro +0 -709
  147. package/templates/default/src/pages/apps/index.astro +0 -669
  148. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,41 +1,317 @@
1
1
  ---
2
- import type { CuratedItem, HealthEntry } from "@grove-dev/core";
3
- import HealthBadge from "./HealthBadge.astro";
2
+ /**
3
+ * ItemCard — generic project/resource/entity card.
4
+ *
5
+ * The most-decorated card in the showcase. Used on home (curated
6
+ * sections), the list page (cards-only views), and the search
7
+ * results. Renders:
8
+ *
9
+ * - Logo/avatar with initials fallback (logoUrl -> GitHub owner
10
+ * avatar -> 2-letter initials).
11
+ * - Owner / repo link (when `repoUrl` is github.com).
12
+ * - Stack + platform chips (first 3, then "+N" overflow).
13
+ * - Optional curated mark (label badge) — e.g. "Featured", "New".
14
+ * - Health badge (Active / Mature / Stale / Archived / ...).
15
+ * - License + star count + last-updated date.
16
+ * - Two CTAs: "View repo" (links.github if present, else repoUrl)
17
+ * and "Details" (the page link).
18
+ *
19
+ * Generic: takes any `Resource` (`kind: "project" | "resource" |
20
+ * "entity"`). For "project" we read the project-style fields
21
+ * (`repoUrl`, `logoUrl`, `stacks`, `platforms`, `github`,
22
+ * `curation`, `health`); for other kinds we degrade gracefully to
23
+ * the minimal view.
24
+ *
25
+ * Index-payload-friendly: when the consumer passes an
26
+ * `IndexProjectRecord` (the slim list-page shape), the component
27
+ * reads the projected fields directly — `github.stars` /
28
+ * `github.pushedAt` / `github.license` — rather than the raw
29
+ * `github.repository` block.
30
+ */
31
+ import type {
32
+ Resource,
33
+ ProjectRecord,
34
+ HealthStatus,
35
+ IndexProjectRecord,
36
+ } from "@grove-dev/core";
37
+ import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../lib/repo";
38
+ import { formatRelative, formatStars } from "../lib/format";
39
+ import { statusDisplay, prettySlug, labelDisplay } from "../lib/display";
4
40
 
5
41
  interface Props {
6
- item: CuratedItem;
7
- health?: HealthEntry;
42
+ record: Resource | IndexProjectRecord;
43
+ /** Detail page href. */
44
+ href: string;
45
+ /** When true, show owner/repo below the title. Default true. */
46
+ showOwner?: boolean;
47
+ /** When true, render the stack/platform chip row. Default true. */
48
+ showChips?: boolean;
49
+ /** When true, render the curated label badge row. Default true. */
50
+ showLabel?: boolean;
51
+ /** When true, render the metadata strip (license/stars/updated). */
52
+ showMeta?: boolean;
53
+ /** Extra class on the outer <article>. */
54
+ class?: string;
8
55
  }
9
56
 
10
- const { item, health } = Astro.props;
11
- const href = `/projects/${item.id}`;
12
- const tags = item.taxonomy.tags.slice(0, 4);
57
+ const {
58
+ record,
59
+ href,
60
+ showOwner = true,
61
+ showChips = true,
62
+ showLabel = true,
63
+ showMeta = true,
64
+ class: className = "",
65
+ } = Astro.props;
66
+
67
+ // Project-specific extraction. The `isIndex` branch reads the slim
68
+ // fields from `toIndexRecord`; the full branch reads the raw
69
+ // `github.repository` block for sites that want max fidelity.
70
+ const isIndex = "github" in record && record.github !== undefined
71
+ && (record as IndexProjectRecord).github !== undefined
72
+ && (record.github as { fullName?: string })?.fullName !== undefined;
73
+ const project = record.kind === "project" ? (record as ProjectRecord | IndexProjectRecord) : null;
74
+
75
+ const name =
76
+ record.kind === "resource"
77
+ ? (record as { title: string }).title
78
+ : (record as { name: string }).name;
79
+ const description = record.description ?? "";
80
+ const tags = (record.tags ?? []).slice(0, 4);
81
+ const repoUrl = (project as ProjectRecord | IndexProjectRecord | null)?.repoUrl;
82
+ const logoUrl = (project as ProjectRecord | IndexProjectRecord | null)?.logoUrl;
83
+ const stacks = (project?.stacks ?? (project?.stack ? [project.stack] : [])) as string[];
84
+ const platforms = (project?.platforms ?? []) as string[];
85
+ const healthStatus: HealthStatus | undefined = project?.health?.status;
86
+ const labels = record.curation?.labels ?? [];
87
+ // Slim (IndexProjectRecord.github) → full (ProjectRecord.github.repository).
88
+ const indexGh = isIndex ? (project as IndexProjectRecord).github : null;
89
+ const fullGh = !isIndex ? (project as ProjectRecord | null)?.github?.repository : null;
90
+ const stars = indexGh?.stars ?? fullGh?.stargazers_count;
91
+ const licenseSpdx = indexGh?.license ?? fullGh?.license?.spdx_id ?? null;
92
+ const pushedAt = indexGh?.pushedAt ?? fullGh?.pushed_at;
93
+ const githubUrl =
94
+ repoUrl ?? (record.links as { github?: string } | undefined)?.github;
95
+
96
+ // Logo fallback chain
97
+ const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
98
+ const avatarSrc =
99
+ logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
100
+ const initials = (name || "")
101
+ .replace(/[^a-z0-9]/gi, "")
102
+ .slice(0, 2)
103
+ .toUpperCase();
104
+
105
+ // Chips: 3 stacks/platforms, then overflow count
106
+ function withOverflow(
107
+ items: string[],
108
+ ): { shown: string[]; overflow: number } {
109
+ const shown = items.filter(Boolean).slice(0, 3);
110
+ const overflow = Math.max(0, items.filter(Boolean).length - shown.length);
111
+ return { shown, overflow };
112
+ }
113
+ const stackChips = withOverflow(stacks);
114
+ const platformChips = withOverflow(platforms);
115
+
116
+ // Curated mark
117
+ const curatedLabel = labels[0] ?? null;
118
+
119
+ // Health class hook (for badge variant). `project` is the union of
120
+ // ProjectRecord | IndexProjectRecord, and both share a `health` field
121
+ // with the same shape, so a single optional-chain read covers both
122
+ // shapes — no isIndex branch is needed for the status itself.
123
+ function healthBadgeClass(s: HealthStatus | undefined): string {
124
+ switch (s) {
125
+ case "mature":
126
+ case "active":
127
+ return "grove-badge-new";
128
+ case "stale":
129
+ case "inactive":
130
+ return "grove-badge-stale";
131
+ case "archived":
132
+ case "unavailable":
133
+ return "grove-badge-hot";
134
+ default:
135
+ return "";
136
+ }
137
+ }
138
+ const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
139
+
140
+ const starsLabel = formatStars(stars);
141
+ const updatedLabel = formatRelative(pushedAt);
13
142
  ---
14
143
 
15
- <article class="oc-card">
16
- <div class="oc-card-header">
17
- <div>
18
- <h2 class="oc-card-title">
19
- <a href={href}>{item.name}</a>
20
- </h2>
21
- <p class="oc-card-category">{item.taxonomy.category}</p>
144
+ <article
145
+ class:list={["grove-card grove-card-hover grove-item-card p-4", className]}
146
+ data-record-slug={record.slug}
147
+ data-record-kind={record.kind}
148
+ data-record-category={record.category}
149
+ data-record-stacks={stacks.join(",")}
150
+ data-record-platforms={platforms.join(",")}
151
+ >
152
+ <div class="flex items-start gap-3">
153
+ <div class="grove-item-card__logo flex-shrink-0">
154
+ {avatarSrc ? (
155
+ <img
156
+ src={avatarSrc}
157
+ alt=""
158
+ width="40"
159
+ height="40"
160
+ loading="lazy"
161
+ decoding="async"
162
+ class="h-10 w-10 rounded-md border border-ink-200 object-contain bg-white dark:border-ink-800 dark:bg-ink-950"
163
+ />
164
+ ) : (
165
+ <span
166
+ class="inline-flex h-10 w-10 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-sm font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
167
+ aria-hidden="true"
168
+ >
169
+ {initials || "·"}
170
+ </span>
171
+ )}
172
+ </div>
173
+
174
+ <div class="min-w-0 flex-1">
175
+ <div class="flex flex-wrap items-baseline gap-2">
176
+ <h2 class="m-0 text-[1rem] font-semibold leading-tight tracking-tight text-ink-900 dark:text-ink-100">
177
+ <a href={href} class="text-inherit no-underline hover:underline">
178
+ {name}
179
+ </a>
180
+ </h2>
181
+ {showLabel && curatedLabel && (
182
+ <span
183
+ class:list={["grove-badge", `grove-badge-${curatedLabel}`]}
184
+ title={`Curated: ${labelDisplay(curatedLabel) ?? curatedLabel}`}
185
+ >
186
+ {labelDisplay(curatedLabel) ?? prettySlug(curatedLabel)}
187
+ </span>
188
+ )}
189
+ {healthLabel && (
190
+ <span class:list={["grove-badge", healthBadgeClass(healthStatus)]}>
191
+ {healthLabel}
192
+ </span>
193
+ )}
194
+ </div>
195
+
196
+ {showOwner && ownerRepo && (
197
+ <p class="m-0 mt-0.5 text-2xs text-ink-500 dark:text-ink-400">
198
+ <a
199
+ href={`https://github.com/${ownerRepo.owner}`}
200
+ target="_blank"
201
+ rel="noopener noreferrer"
202
+ class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
203
+ >
204
+ @{ownerRepo.owner}
205
+ </a>
206
+ <span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
207
+ <a
208
+ href={repoUrl}
209
+ target="_blank"
210
+ rel="noopener noreferrer"
211
+ class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
212
+ >
213
+ {ownerRepo.repo}
214
+ </a>
215
+ </p>
216
+ )}
217
+
218
+ {description && (
219
+ <p class="m-0 mt-1.5 line-clamp-2 text-[0.8125rem] leading-snug text-ink-500 dark:text-ink-400">
220
+ {description}
221
+ </p>
222
+ )}
22
223
  </div>
23
- <HealthBadge status={health?.health.status} />
24
- </div>
25
- <p class="oc-card-description">{item.description}</p>
26
- <div class="oc-card-meta">
27
- {health?.github?.stars != null && <span>{health.github.stars.toLocaleString()} stars</span>}
28
- {health?.github?.license && <span>{health.github.license}</span>}
29
- {health?.github?.language && <span>{health.github.language}</span>}
30
- </div>
31
- <div class="oc-card-actions">
32
- <a href={`/projects/${item.id}`}>Details</a>
33
- {item.links.github && <a href={item.links.github}>GitHub</a>}
34
- {item.links.docs && <a href={item.links.docs}>Docs</a>}
35
224
  </div>
36
- {tags.length > 0 && (
37
- <ul class="oc-tags" aria-label="Tags">
38
- {tags.map((tag) => <li>{tag}</li>)}
39
- </ul>
225
+
226
+ {showChips && (stackChips.shown.length > 0 || platformChips.shown.length > 0) && (
227
+ <div class="mt-3 flex flex-wrap items-center gap-1.5">
228
+ {stackChips.shown.map((s) => (
229
+ <span class="grove-badge" title={`Stack: ${s}`}>{s}</span>
230
+ ))}
231
+ {stackChips.overflow > 0 && (
232
+ <span class="grove-badge" title={`${stackChips.overflow} more stacks`}>
233
+ +{stackChips.overflow}
234
+ </span>
235
+ )}
236
+ {platformChips.shown.map((p) => (
237
+ <span class="grove-badge" title={`Platform: ${p}`}>{p}</span>
238
+ ))}
239
+ {platformChips.overflow > 0 && (
240
+ <span
241
+ class="grove-badge"
242
+ title={`${platformChips.overflow} more platforms`}
243
+ >
244
+ +{platformChips.overflow}
245
+ </span>
246
+ )}
247
+ {tags.length > 0 && tags.map((t) => <span class="grove-badge">{t}</span>)}
248
+ </div>
40
249
  )}
250
+
251
+ {showMeta && (starsLabel || licenseSpdx || updatedLabel !== "—") && (
252
+ <dl
253
+ class="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400"
254
+ aria-label="Project signals"
255
+ >
256
+ {starsLabel && (
257
+ <div class="flex items-center gap-1">
258
+ <dt class="sr-only">Stars</dt>
259
+ <dd class="m-0 font-mono">{starsLabel}</dd>
260
+ <span aria-hidden="true">★</span>
261
+ </div>
262
+ )}
263
+ {licenseSpdx && (
264
+ <div class="flex items-center gap-1">
265
+ <dt class="sr-only">License</dt>
266
+ <dd class="m-0">{licenseSpdx}</dd>
267
+ </div>
268
+ )}
269
+ {updatedLabel && updatedLabel !== "—" && (
270
+ <div class="flex items-center gap-1">
271
+ <dt class="sr-only">Last updated</dt>
272
+ <dd class="m-0">Updated {updatedLabel}</dd>
273
+ </div>
274
+ )}
275
+ </dl>
276
+ )}
277
+
278
+ <div class="mt-4 flex flex-wrap items-center gap-2">
279
+ <a
280
+ href={href}
281
+ class="grove-btn grove-btn-outline"
282
+ data-event="card_details_click"
283
+ data-event-source="item_card"
284
+ data-event-app={record.slug}
285
+ >
286
+ Details
287
+ </a>
288
+ {githubUrl && (
289
+ <a
290
+ href={githubUrl}
291
+ target="_blank"
292
+ rel="noopener noreferrer"
293
+ class="grove-btn grove-btn-ghost"
294
+ data-event="card_repo_click"
295
+ data-event-source="item_card"
296
+ data-event-app={record.slug}
297
+ >
298
+ View repo
299
+ </a>
300
+ )}
301
+ </div>
41
302
  </article>
303
+
304
+ <style>
305
+ .grove-item-card {
306
+ display: flex;
307
+ flex-direction: column;
308
+ gap: 0.25rem;
309
+ height: 100%;
310
+ }
311
+ .line-clamp-2 {
312
+ display: -webkit-box;
313
+ -webkit-line-clamp: 2;
314
+ -webkit-box-orient: vertical;
315
+ overflow: hidden;
316
+ }
317
+ </style>
@@ -0,0 +1,82 @@
1
+ ---
2
+ /**
3
+ * MinimalAbout — 3 short points + title, used on the about page.
4
+ *
5
+ * A trimmed-down sibling of `WhyThisExists` with a different layout
6
+ * (no icons, no "0X" counter, vertical list) for text-heavy pages.
7
+ *
8
+ * Generic: every point comes through props.
9
+ */
10
+
11
+ interface AboutPoint {
12
+ title: string;
13
+ body: string;
14
+ }
15
+
16
+ interface Props {
17
+ points?: AboutPoint[];
18
+ title?: string;
19
+ description?: string;
20
+ class?: string;
21
+ }
22
+
23
+ const {
24
+ points,
25
+ title = "About this directory",
26
+ description,
27
+ class: className = "",
28
+ } = Astro.props;
29
+
30
+ const defaultPoints: AboutPoint[] = [
31
+ {
32
+ title: "Curated, not scraped",
33
+ body: "Every entry is hand-checked. If something here, it's because someone looked at the source, ran the build, and decided it was worth your time.",
34
+ },
35
+ {
36
+ title: "Plain text all the way down",
37
+ body: "Records live in plain YAML, content in plain Markdown, and the static output in plain HTML. No database, no JavaScript framework, no CDN with sticky sessions.",
38
+ },
39
+ {
40
+ title: "Free to fork, free to extend",
41
+ body: "This whole site is one repo. If you want your own copy, fork it. If you want to add a record, open a PR. The directory is the community.",
42
+ },
43
+ ];
44
+
45
+ const items = points && points.length > 0 ? points : defaultPoints;
46
+ ---
47
+
48
+ <section class:list={["grove-section", className]}>
49
+ <div class="grove-container-narrow mx-auto px-5 sm:px-7">
50
+ <header class="mb-6 flex flex-col gap-2">
51
+ <h2 class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
52
+ {title}
53
+ </h2>
54
+ {description && (
55
+ <p class="m-0 text-[0.9375rem] text-ink-500 dark:text-ink-400">
56
+ {description}
57
+ </p>
58
+ )}
59
+ </header>
60
+
61
+ <ol class="m-0 flex list-none flex-col gap-5 p-0">
62
+ {items.map((p, i) => (
63
+ <li class="flex gap-4">
64
+ <span
65
+ aria-hidden="true"
66
+ class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 font-mono text-2xs text-ink-500 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-400"
67
+ >
68
+ 0{i + 1}
69
+ </span>
70
+ <div>
71
+ <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
72
+ {p.title}
73
+ </h3>
74
+ <p class="m-0 mt-1 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400">
75
+ {p.body}
76
+ </p>
77
+ </div>
78
+ </li>
79
+ ))}
80
+ </ol>
81
+ </div>
82
+ </section>
@@ -0,0 +1,116 @@
1
+ ---
2
+ /**
3
+ * OriginalCollection — legacy lineage card.
4
+ *
5
+ * Renders a single wide card that links out to the project's origin
6
+ * repository, with stars/forks/contributors stats. Used on home and
7
+ * about pages to give visitors a way to find the original curated
8
+ * collection this directory descends from.
9
+ *
10
+ * Generic: takes plain `stats` and a `repoUrl`. No blueprint or
11
+ * site-specific copy.
12
+ */
13
+
14
+ interface OriginStats {
15
+ stars?: number;
16
+ forks?: number;
17
+ contributors?: number;
18
+ description?: string;
19
+ }
20
+
21
+ interface Props {
22
+ repoUrl: string;
23
+ repoLabel?: string;
24
+ stats?: OriginStats;
25
+ title?: string;
26
+ description?: string;
27
+ class?: string;
28
+ }
29
+
30
+ const {
31
+ repoUrl,
32
+ repoLabel = "View on GitHub",
33
+ stats = {},
34
+ title = "A growing list, forked from the original",
35
+ description = "This directory descends from an earlier curated collection. The original still lives on — same shape, same spirit, fresh data.",
36
+ class: className = "",
37
+ } = Astro.props;
38
+
39
+ function fmt(n: number | undefined): string | null {
40
+ if (typeof n !== "number" || !Number.isFinite(n)) return null;
41
+ if (n >= 1000) {
42
+ const k = n / 1000;
43
+ return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
44
+ }
45
+ return n.toString();
46
+ }
47
+
48
+ const hasStats =
49
+ stats.stars !== undefined ||
50
+ stats.forks !== undefined ||
51
+ stats.contributors !== undefined;
52
+ ---
53
+
54
+ <section class:list={["grove-section", className]}>
55
+ <div class="grove-container mx-auto px-5 sm:px-7">
56
+ <div
57
+ class="grove-card flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between"
58
+ >
59
+ <div class="flex flex-col gap-2">
60
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
61
+ Origin
62
+ </span>
63
+ <h2 class="m-0 text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
64
+ {title}
65
+ </h2>
66
+ <p class="m-0 max-w-2xl text-[0.875rem] text-ink-500 dark:text-ink-400">
67
+ {description}
68
+ </p>
69
+ {stats.description && (
70
+ <p class="m-0 max-w-2xl text-[0.8125rem] text-ink-500 dark:text-ink-400">
71
+ {stats.description}
72
+ </p>
73
+ )}
74
+ {hasStats && (
75
+ <dl
76
+ class="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-2xs text-ink-500 dark:text-ink-400"
77
+ aria-label="Original repository signals"
78
+ >
79
+ {stats.stars !== undefined && (
80
+ <div class="flex items-center gap-1">
81
+ <dd class="m-0 font-mono">{fmt(stats.stars) ?? stats.stars}</dd>
82
+ <dt>stars</dt>
83
+ </div>
84
+ )}
85
+ {stats.forks !== undefined && (
86
+ <div class="flex items-center gap-1">
87
+ <dd class="m-0 font-mono">{fmt(stats.forks) ?? stats.forks}</dd>
88
+ <dt>forks</dt>
89
+ </div>
90
+ )}
91
+ {stats.contributors !== undefined && (
92
+ <div class="flex items-center gap-1">
93
+ <dd class="m-0 font-mono">
94
+ {fmt(stats.contributors) ?? stats.contributors}
95
+ </dd>
96
+ <dt>contributors</dt>
97
+ </div>
98
+ )}
99
+ </dl>
100
+ )}
101
+ </div>
102
+ <div class="flex flex-shrink-0 items-center gap-2">
103
+ <a
104
+ href={repoUrl}
105
+ target="_blank"
106
+ rel="noopener noreferrer"
107
+ class="grove-btn grove-btn-primary"
108
+ data-event="original_collection_click"
109
+ data-event-source="original_collection"
110
+ >
111
+ {repoLabel} →
112
+ </a>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </section>
@@ -0,0 +1,88 @@
1
+ ---
2
+ /**
3
+ * RecordSection — a titled "lens" of records on the home page.
4
+ *
5
+ * Renders a SectionHeader + a 3-column grid of `ItemCard`s, with an
6
+ * optional "View all" link in the header. Generic: passes the
7
+ * record list + per-record href through to `ItemCard`.
8
+ *
9
+ * The component is the workhorse of the home page — featured, hot,
10
+ * new, mature, "by category", "by stack" — all of them are just a
11
+ * `RecordSection` with a different `id` + `title` + sliced list.
12
+ *
13
+ * V1: items are `IndexProjectRecord`s (the slim list-page shape).
14
+ * Other blueprints (`resource-hub`, `ecosystem-map`) can pass their
15
+ * own `IndexResourceRecord` / `IndexEntityRecord` arrays; the
16
+ * component does not branch on the kind.
17
+ */
18
+
19
+ import type { IndexProjectRecord } from "@grove-dev/core";
20
+ import ItemCard from "./ItemCard.astro";
21
+ import SectionHeader from "../layouts/SectionHeader.astro";
22
+
23
+ interface Props {
24
+ /** Used as the section id (so deep links work) and a stable key. */
25
+ id: string;
26
+ title: string;
27
+ description?: string;
28
+ items: IndexProjectRecord[];
29
+ /** Detail page href builder. */
30
+ hrefFor: (record: IndexProjectRecord) => string;
31
+ /** Optional "View all" link rendered in the header. */
32
+ viewAllHref?: string;
33
+ viewAllLabel?: string;
34
+ /** Optional eyebrow. */
35
+ eyebrow?: string;
36
+ /** Empty-state copy. */
37
+ emptyLabel?: string;
38
+ /** Extra class on the outer <section>. */
39
+ class?: string;
40
+ }
41
+
42
+ const {
43
+ id,
44
+ title,
45
+ description,
46
+ items,
47
+ hrefFor,
48
+ viewAllHref,
49
+ viewAllLabel = "View all",
50
+ eyebrow,
51
+ emptyLabel = "No records yet.",
52
+ class: className = "",
53
+ } = Astro.props;
54
+ ---
55
+
56
+ <section
57
+ id={id}
58
+ class:list={["grove-section", className]}
59
+ aria-labelledby={`${id}-heading`}
60
+ >
61
+ <div class="grove-container-wide mx-auto px-5 sm:px-7">
62
+ <SectionHeader
63
+ eyebrow={eyebrow}
64
+ title={title}
65
+ description={description}
66
+ >
67
+ {viewAllHref && (
68
+ <a
69
+ slot="actions"
70
+ href={viewAllHref}
71
+ class="text-sm font-medium text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
72
+ >
73
+ {viewAllLabel} →
74
+ </a>
75
+ )}
76
+ </SectionHeader>
77
+
78
+ {items.length === 0 ? (
79
+ <p class="m-0 text-sm text-ink-500 dark:text-ink-400">{emptyLabel}</p>
80
+ ) : (
81
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
82
+ {items.map((record) => (
83
+ <ItemCard record={record} href={hrefFor(record)} />
84
+ ))}
85
+ </div>
86
+ )}
87
+ </div>
88
+ </section>