@grove-dev/astro 0.2.20 → 0.3.2

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -75
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/display.d.ts +1 -26
  7. package/dist/lib/display.d.ts.map +1 -1
  8. package/dist/lib/display.js +1 -103
  9. package/dist/lib/display.js.map +1 -1
  10. package/dist/lib/format.d.ts +1 -28
  11. package/dist/lib/format.d.ts.map +1 -1
  12. package/dist/lib/format.js +1 -71
  13. package/dist/lib/format.js.map +1 -1
  14. package/dist/lib/lenses.d.ts +1 -57
  15. package/dist/lib/lenses.d.ts.map +1 -1
  16. package/dist/lib/lenses.js +1 -198
  17. package/dist/lib/lenses.js.map +1 -1
  18. package/dist/lib/repo.d.ts +1 -19
  19. package/dist/lib/repo.d.ts.map +1 -1
  20. package/dist/lib/repo.js +1 -30
  21. package/dist/lib/repo.js.map +1 -1
  22. package/dist/lib/scores.d.ts +1 -36
  23. package/dist/lib/scores.d.ts.map +1 -1
  24. package/dist/lib/scores.js +1 -75
  25. package/dist/lib/scores.js.map +1 -1
  26. package/dist/lib/search.d.ts +1 -113
  27. package/dist/lib/search.d.ts.map +1 -1
  28. package/dist/lib/search.js +1 -379
  29. package/dist/lib/search.js.map +1 -1
  30. package/dist/lib/taxonomy-counts.d.ts +1 -10
  31. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  32. package/dist/lib/taxonomy-counts.js +1 -21
  33. package/dist/lib/taxonomy-counts.js.map +1 -1
  34. package/package.json +22 -10
  35. package/src/components/ContributorsGrid.astro +3 -3
  36. package/src/components/CurationGrid.astro +2 -2
  37. package/src/components/DirectoryIndexClient.astro +240 -0
  38. package/src/components/FilterGroupMenu.astro +8 -4
  39. package/src/components/FinalCta.astro +41 -0
  40. package/src/components/Hero.astro +15 -18
  41. package/src/components/Icon.astro +99 -27
  42. package/src/components/RefinePanel.astro +7 -7
  43. package/src/components/SmartLensTabs.astro +5 -4
  44. package/src/components/StackGrid.astro +1 -1
  45. package/src/components/SubmissionClient.astro +133 -0
  46. package/src/index.ts +11 -1
  47. package/src/layouts/BaseLayout.astro +19 -1
  48. package/src/layouts/Footer.astro +25 -56
  49. package/src/layouts/Header.astro +33 -15
  50. package/src/layouts/ThemeToggle.astro +7 -3
  51. package/src/lib/display.ts +1 -111
  52. package/src/lib/format.ts +1 -64
  53. package/src/lib/lenses.ts +1 -220
  54. package/src/lib/repo.ts +1 -32
  55. package/src/lib/scores.ts +1 -90
  56. package/src/lib/search.ts +1 -418
  57. package/src/lib/taxonomy-counts.ts +1 -26
  58. package/{templates/default/src/data/records.ts → src/server/directory.ts} +90 -8
  59. package/src/server/index.ts +2 -0
  60. package/src/server/models.ts +432 -0
  61. package/dist/theme.test.d.ts +0 -2
  62. package/dist/theme.test.d.ts.map +0 -1
  63. package/dist/theme.test.js +0 -55
  64. package/dist/theme.test.js.map +0 -1
  65. package/src/theme.test.ts +0 -67
  66. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  67. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  68. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
  69. package/templates/default/.github/pull_request_template.md +0 -27
  70. package/templates/default/.github/workflows/build.yml +0 -55
  71. package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -48
  72. package/templates/default/.github/workflows/daily-refresh.yml +0 -53
  73. package/templates/default/.github/workflows/sync-contributors.yml +0 -53
  74. package/templates/default/.github/workflows/sync-github-metadata.yml +0 -58
  75. package/templates/default/.github/workflows/validate-data.yml +0 -35
  76. package/templates/default/astro.config.mjs +0 -25
  77. package/templates/default/content/pages/about.md +0 -66
  78. package/templates/default/content/pages/methodology.md +0 -103
  79. package/templates/default/data/records/.gitkeep +0 -0
  80. package/templates/default/data/records/coolify.yml +0 -46
  81. package/templates/default/data/records/gitea.yml +0 -46
  82. package/templates/default/data/records/hoppscotch.yml +0 -47
  83. package/templates/default/data/records/inventree.yml +0 -47
  84. package/templates/default/data/records/label-studio.yml +0 -46
  85. package/templates/default/data/records/logseq.yml +0 -48
  86. package/templates/default/data/records/private-gpt.yml +0 -47
  87. package/templates/default/data/records/wakapi.yml +0 -44
  88. package/templates/default/data/taxonomy/categories.yml +0 -47
  89. package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
  90. package/templates/default/data/taxonomy/platforms.yml +0 -27
  91. package/templates/default/data/taxonomy/stacks.yml +0 -41
  92. package/templates/default/grove.config.ts +0 -36
  93. package/templates/default/package.json +0 -38
  94. package/templates/default/public/icons/platforms/android.svg +0 -1
  95. package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
  96. package/templates/default/public/icons/platforms/apple-light.svg +0 -1
  97. package/templates/default/public/icons/platforms/chrome.svg +0 -1
  98. package/templates/default/public/icons/platforms/chromeos.svg +0 -5
  99. package/templates/default/public/icons/platforms/desktop.svg +0 -4
  100. package/templates/default/public/icons/platforms/embedded.svg +0 -5
  101. package/templates/default/public/icons/platforms/ios.svg +0 -1
  102. package/templates/default/public/icons/platforms/linux.svg +0 -1
  103. package/templates/default/public/icons/platforms/macos.svg +0 -1
  104. package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
  105. package/templates/default/public/icons/platforms/web.svg +0 -5
  106. package/templates/default/public/icons/platforms/windows.svg +0 -6
  107. package/templates/default/public/icons/stacks/android.svg +0 -1
  108. package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
  109. package/templates/default/public/icons/stacks/apple-light.svg +0 -1
  110. package/templates/default/public/icons/stacks/bun.svg +0 -1
  111. package/templates/default/public/icons/stacks/capacitor.svg +0 -14
  112. package/templates/default/public/icons/stacks/clojure.svg +0 -1
  113. package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
  114. package/templates/default/public/icons/stacks/dart.svg +0 -1
  115. package/templates/default/public/icons/stacks/deno.svg +0 -1
  116. package/templates/default/public/icons/stacks/django.svg +0 -1
  117. package/templates/default/public/icons/stacks/docker.svg +0 -1
  118. package/templates/default/public/icons/stacks/firebase.svg +0 -1
  119. package/templates/default/public/icons/stacks/flutter.svg +0 -1
  120. package/templates/default/public/icons/stacks/go.svg +0 -1
  121. package/templates/default/public/icons/stacks/graphql.svg +0 -1
  122. package/templates/default/public/icons/stacks/ionic.svg +0 -4
  123. package/templates/default/public/icons/stacks/java.svg +0 -1
  124. package/templates/default/public/icons/stacks/javascript.svg +0 -1
  125. package/templates/default/public/icons/stacks/kotlin.svg +0 -1
  126. package/templates/default/public/icons/stacks/llm.svg +0 -1
  127. package/templates/default/public/icons/stacks/mongodb.svg +0 -1
  128. package/templates/default/public/icons/stacks/node.js.svg +0 -1
  129. package/templates/default/public/icons/stacks/nodejs.svg +0 -1
  130. package/templates/default/public/icons/stacks/python.svg +0 -1
  131. package/templates/default/public/icons/stacks/rag.svg +0 -7
  132. package/templates/default/public/icons/stacks/react-native.svg +0 -1
  133. package/templates/default/public/icons/stacks/react.svg +0 -1
  134. package/templates/default/public/icons/stacks/rust.svg +0 -1
  135. package/templates/default/public/icons/stacks/solidity.svg +0 -1
  136. package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
  137. package/templates/default/public/icons/stacks/swift.svg +0 -1
  138. package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
  139. package/templates/default/public/icons/stacks/typescript.svg +0 -1
  140. package/templates/default/public/icons/stacks/vue.svg +0 -1
  141. package/templates/default/public/llms-full.txt +0 -206
  142. package/templates/default/public/llms.txt +0 -112
  143. package/templates/default/public/og-image.svg +0 -48
  144. package/templates/default/public/robots.txt +0 -53
  145. package/templates/default/public/sitemap.xml +0 -63
  146. package/templates/default/scripts/build-llms.mjs +0 -123
  147. package/templates/default/scripts/build-records-json.mjs +0 -27
  148. package/templates/default/scripts/build-sitemap.mjs +0 -20
  149. package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
  150. package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
  151. package/templates/default/scripts/fetch-icons.mjs +0 -137
  152. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
  153. package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
  154. package/templates/default/scripts/refresh-records-activity.mjs +0 -24
  155. package/templates/default/scripts/repair-license-format.mjs +0 -72
  156. package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
  157. package/templates/default/scripts/seed-from-github.mjs +0 -62
  158. package/templates/default/scripts/sync-contributors.mjs +0 -145
  159. package/templates/default/scripts/sync-github-metadata.mjs +0 -28
  160. package/templates/default/scripts/validate-records.mjs +0 -27
  161. package/templates/default/src/pages/404.astro +0 -64
  162. package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -689
  163. package/templates/default/src/pages/[slug]/index.astro +0 -278
  164. package/templates/default/src/pages/about.astro +0 -182
  165. package/templates/default/src/pages/apps/[recordSlug].astro +0 -33
  166. package/templates/default/src/pages/contributors.astro +0 -175
  167. package/templates/default/src/pages/index.astro +0 -225
  168. package/templates/default/src/pages/sitemap.xml.ts +0 -89
  169. package/templates/default/src/pages/submit.astro +0 -281
  170. package/templates/default/src/styles/global.css +0 -33
  171. package/templates/default/wrangler.jsonc +0 -28
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Page view-models for Grove's Astro adapter.
3
+ *
4
+ * These functions contain directory decisions and generated-data access,
5
+ * while consumer-owned Astro pages keep control of markup and composition.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import type { IndexFilters, IndexRecord, ProjectRecord } from "@grove-dev/core";
10
+ import {
11
+ activeFilterChips,
12
+ applySort,
13
+ buildFacets,
14
+ effectivePage,
15
+ effectiveSort,
16
+ filterRecords,
17
+ filtersFromSearchParams,
18
+ formatRelative,
19
+ formatStars,
20
+ getOwnerAndRepoFromRepoUrl,
21
+ getOwnerAvatarUrl,
22
+ statusDisplay,
23
+ totalPages,
24
+ } from "@grove-dev/core";
25
+ import {
26
+ entityBySlug,
27
+ fullItems,
28
+ fullProjects,
29
+ getContentHtml,
30
+ itemLabel,
31
+ itemLabelPlural,
32
+ items,
33
+ projectBySlug,
34
+ resourceBySlug,
35
+ taxonomyLabel,
36
+ } from "./directory.js";
37
+
38
+ export interface DirectorySiteConfig {
39
+ name: string;
40
+ tagline?: string;
41
+ description?: string;
42
+ repoUrl?: string;
43
+ nav?: Array<{ label: string; href: string }>;
44
+ facets?: string[];
45
+ footer?: {
46
+ columns?: Array<{
47
+ heading: string;
48
+ items: Array<{ label: string; href: string; external?: boolean }>;
49
+ }>;
50
+ copyright?: string;
51
+ license?: string;
52
+ };
53
+ submission?: {
54
+ eyebrow?: string;
55
+ title?: string;
56
+ description?: string;
57
+ good?: string[];
58
+ avoid?: string[];
59
+ };
60
+ blueprintConfig?: {
61
+ routeSlug?: string;
62
+ labelSingular?: string;
63
+ labelPlural?: string;
64
+ };
65
+ taxonomy?: {
66
+ categories?: Array<{ id: string; name: string }>;
67
+ stacks?: Array<{ id: string; name: string }>;
68
+ platforms?: Array<{ id: string; name: string }>;
69
+ };
70
+ stats?: {
71
+ originalRepo?: string;
72
+ originalStars?: number;
73
+ originalForks?: number;
74
+ originalContributors?: number;
75
+ totalApps?: number;
76
+ totalCategories?: number;
77
+ totalStacks?: number;
78
+ totalOwners?: number;
79
+ totalStars?: number;
80
+ };
81
+ }
82
+
83
+ export interface DirectoryContributor {
84
+ username: string;
85
+ name?: string;
86
+ avatarUrl?: string;
87
+ profileUrl?: string;
88
+ contributions?: number;
89
+ }
90
+
91
+ export function loadDirectoryContributors(root = process.cwd()): DirectoryContributor[] {
92
+ const path = resolve(root, "data", "generated", "contributors.json");
93
+ if (!existsSync(path)) return [];
94
+ try {
95
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
96
+ contributors?: DirectoryContributor[];
97
+ };
98
+ return parsed.contributors ?? [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ export function getHomePageModel(site: DirectorySiteConfig) {
105
+ const slug = site.blueprintConfig?.routeSlug ?? "projects";
106
+ const singular = site.blueprintConfig?.labelSingular ?? itemLabel();
107
+ const plural = site.blueprintConfig?.labelPlural ?? itemLabelPlural();
108
+ const projects = items.filter((record) => record.kind === "project");
109
+ const hot = filterRecords(projects, { labels: ["hot"] }).slice(0, 6);
110
+ const recentlyAdded = applySort(projects, "recently-added").slice(0, 6);
111
+ const established = filterRecords(projects, { labels: ["mature"] }).slice(0, 6);
112
+
113
+ const stackCounts = new Map<string, number>();
114
+ const categoryCounts = new Map<string, number>();
115
+ for (const record of fullItems) {
116
+ if (record.category) categoryCounts.set(record.category, (categoryCounts.get(record.category) ?? 0) + 1);
117
+ if (record.kind === "project" && record.stack) {
118
+ stackCounts.set(record.stack, (stackCounts.get(record.stack) ?? 0) + 1);
119
+ }
120
+ }
121
+ const stacks = [...stackCounts]
122
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
123
+ .slice(0, 12)
124
+ .map(([id, count]) => ({ name: taxonomyLabel("stacks", id), slug: id, count }));
125
+ const categories = [...categoryCounts]
126
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
127
+ .map(([id, count]) => ({ name: taxonomyLabel("categories", id), slug: id, count }));
128
+ const contributors = loadDirectoryContributors();
129
+ const description = site.description ??
130
+ `A searchable directory of real ${plural} — organized by stack, category, platform, license, activity, and maturity.`;
131
+
132
+ return {
133
+ slug,
134
+ itemSingular: singular,
135
+ itemPlural: plural,
136
+ hot,
137
+ recentlyAdded,
138
+ established,
139
+ stacks,
140
+ categories,
141
+ contributors,
142
+ title: `${site.name} — ${site.tagline ?? ""}`.trim(),
143
+ description,
144
+ stats: {
145
+ originalRepo: site.stats?.originalRepo ?? "",
146
+ apps: site.stats?.totalApps ?? 0,
147
+ categories: site.stats?.totalCategories ?? 0,
148
+ stacks: site.stats?.totalStacks ?? 0,
149
+ owners: site.stats?.totalOwners ?? 0,
150
+ stars: site.stats?.totalStars ?? 0,
151
+ },
152
+ };
153
+ }
154
+
155
+ const FACET_ALIASES: Record<string, "stacks" | "platforms" | "categories" | "tags" | "licenses"> = {
156
+ stack: "stacks",
157
+ stacks: "stacks",
158
+ platform: "platforms",
159
+ platforms: "platforms",
160
+ category: "categories",
161
+ categories: "categories",
162
+ tag: "tags",
163
+ tags: "tags",
164
+ license: "licenses",
165
+ licenses: "licenses",
166
+ };
167
+
168
+ function configuredFacets(site?: DirectorySiteConfig) {
169
+ return new Set((site?.facets ?? ["category", "tags"])
170
+ .map((facet) => FACET_ALIASES[facet])
171
+ .filter((facet): facet is NonNullable<typeof facet> => Boolean(facet)));
172
+ }
173
+
174
+ export function getDirectoryIndexModel(searchParams: URLSearchParams, site?: DirectorySiteConfig) {
175
+ const filters = filtersFromSearchParams(searchParams);
176
+ const rawFacets = buildFacets(items);
177
+ const enabled = configuredFacets(site);
178
+ const facets = {
179
+ stacks: enabled.has("stacks") ? rawFacets.stacks.map((option) => ({ ...option, label: taxonomyLabel("stacks", option.value) })) : [],
180
+ platforms: enabled.has("platforms") ? rawFacets.platforms.map((option) => ({ ...option, label: taxonomyLabel("platforms", option.value) })) : [],
181
+ categories: enabled.has("categories") ? rawFacets.categories.map((option) => ({ ...option, label: taxonomyLabel("categories", option.value) })) : [],
182
+ tags: enabled.has("tags") ? rawFacets.tags : [],
183
+ licenses: enabled.has("licenses") ? rawFacets.licenses : [],
184
+ };
185
+ const sort = effectiveSort(filters);
186
+ const sorted = applySort(filterRecords(items, filters), sort);
187
+ const pageCount = totalPages(sorted.length);
188
+ const page = Math.min(effectivePage(filters), pageCount);
189
+ return {
190
+ items,
191
+ total: items.length,
192
+ filters,
193
+ facets,
194
+ sort,
195
+ sorted,
196
+ pages: pageCount,
197
+ page,
198
+ chips: activeFilterChips(filters),
199
+ clientItemsJson: JSON.stringify(items).replace(/</g, "\\u003c"),
200
+ };
201
+ }
202
+
203
+ export function getContributorsPageModel(site: DirectorySiteConfig) {
204
+ const contributors = loadDirectoryContributors();
205
+ const sorted = [...contributors].sort((a, b) =>
206
+ (b.contributions ?? 0) - (a.contributions ?? 0) || a.username.localeCompare(b.username)
207
+ );
208
+ const repo = site.repoUrl?.replace(/\/$/, "");
209
+ return {
210
+ contributors: sorted,
211
+ total: sorted.length,
212
+ title: `Community — ${site.name}`,
213
+ description: `The people who have contributed to ${site.name} through code, curation, and review.`,
214
+ contributorsGraphHref: repo ? `${repo}/graphs/contributors` : null,
215
+ };
216
+ }
217
+
218
+ export function getSubmissionPageModel(site: DirectorySiteConfig) {
219
+ const singular = site.blueprintConfig?.labelSingular ?? itemLabel();
220
+ const enabled = configuredFacets(site);
221
+ const values = (key: "category" | "stack") =>
222
+ [...new Set(fullProjects.map((record) => String(record[key] ?? "")).filter(Boolean))]
223
+ .sort((a, b) => a.localeCompare(b));
224
+ const categories = (site.taxonomy?.categories?.length
225
+ ? site.taxonomy.categories.map(({ id }) => id)
226
+ : values("category"))
227
+ .map((id) => ({ id, label: taxonomyLabel("categories", id) }));
228
+ const stacks = (site.taxonomy?.stacks?.length
229
+ ? site.taxonomy.stacks.map(({ id }) => id)
230
+ : values("stack"))
231
+ .map((id) => ({ id, label: taxonomyLabel("stacks", id) }));
232
+ const existingFullNames = fullProjects.map((record) => {
233
+ const url = String(record.repoUrl ?? record.links?.github ?? "");
234
+ const match = url.match(/github\.com\/([^/]+)\/([^/?#]+)/i);
235
+ return match ? `${match[1]}/${match[2].replace(/\.git$/, "")}`.toLowerCase() : null;
236
+ }).filter((value): value is string => Boolean(value));
237
+
238
+ return {
239
+ singular,
240
+ title: `Submit ${singular} - ${site.name}`,
241
+ description: `Submit a new ${singular} to ${site.name}.`,
242
+ repoUrl: site.repoUrl ?? "https://github.com/tortuvshin/grove",
243
+ copy: site.submission ?? {},
244
+ fields: {
245
+ category: enabled.has("categories"),
246
+ stack: enabled.has("stacks"),
247
+ platforms: enabled.has("platforms"),
248
+ tags: enabled.has("tags"),
249
+ },
250
+ categories,
251
+ stacks,
252
+ existingSlugs: fullProjects.map((record) => record.slug).filter(Boolean),
253
+ existingFullNames,
254
+ platformOptions: site.taxonomy?.platforms?.map(({ id, name }) => ({ id, label: name })) ??
255
+ ["ios", "android", "web", "macos", "windows", "linux"].map((id) => ({ id, label: taxonomyLabel("platforms", id) })),
256
+ };
257
+ }
258
+
259
+ type MonthlyCommit = number | { month: string; commits: number };
260
+ type ProjectExtras = {
261
+ github?: {
262
+ activity?: { monthlyCommits?: MonthlyCommit[] };
263
+ files?: Record<string, boolean>;
264
+ languages?: Record<string, number>;
265
+ };
266
+ health?: { status?: string; tier?: string };
267
+ };
268
+
269
+ export function getRecordDetailModel(
270
+ recordSlug: string,
271
+ site: DirectorySiteConfig,
272
+ routeSlug = site.blueprintConfig?.routeSlug ?? "projects",
273
+ ) {
274
+ const project = projectBySlug(recordSlug);
275
+ const resource = resourceBySlug(recordSlug);
276
+ const entity = entityBySlug(recordSlug);
277
+ const record = project ?? resource ?? entity;
278
+ if (!record) return null;
279
+
280
+ const isProject = record.kind === "project";
281
+ const proj: ProjectRecord | null = project ?? null;
282
+ const name = record.kind === "resource" ? record.title : record.name;
283
+ const description = record.description ?? `${name} on ${site.name}`;
284
+ const repoUrl = proj?.repoUrl ?? record.links?.github ?? "";
285
+ const homepageUrl = record.links?.website ?? "";
286
+ const stacks = proj?.stacks ?? (proj?.stack ? [proj.stack] : []);
287
+ const platforms = proj?.platforms ?? [];
288
+ const github = proj?.github?.repository;
289
+ const stars = github?.stargazers_count ?? 0;
290
+ const forks = github?.forks_count ?? 0;
291
+ const language = github?.language ?? null;
292
+ const licenseSpdx = github?.license?.spdx_id ?? null;
293
+ const pushedAt = github?.pushed_at ?? null;
294
+ const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
295
+ const avatarSrc = proj?.logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
296
+ const rawScores = proj?.scores;
297
+ const scores = rawScores && Object.values(rawScores).some((value) => typeof value === "number")
298
+ ? rawScores
299
+ : null;
300
+ const healthStatus = proj?.health?.status;
301
+ const extras = record as ProjectExtras;
302
+ const monthlyCommits = (extras.github?.activity?.monthlyCommits ?? []).map(
303
+ (value): { month: string; commits: number } => typeof value === "number"
304
+ ? { month: "", commits: value }
305
+ : { month: value.month ?? "", commits: value.commits ?? 0 },
306
+ );
307
+ const contributionLabels: Record<string, string> = {
308
+ readme: "README",
309
+ contributing: "Contributing",
310
+ security: "Security",
311
+ issueTemplates: "Issue templates",
312
+ pullRequestTemplate: "PR template",
313
+ };
314
+ const contributionFiles = extras.github?.files ?? {};
315
+ const contributionSignals = Object.keys(contributionLabels)
316
+ .map((key) => ({ key, label: contributionLabels[key], ok: contributionFiles[key] }))
317
+ .filter((signal) => signal.ok !== undefined);
318
+ const languages = extras.github?.languages
319
+ ? Object.entries(extras.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
320
+ : [];
321
+
322
+ let jsonLd: Record<string, unknown>;
323
+ if (isProject && proj) {
324
+ const sameAs = [repoUrl, homepageUrl].filter(Boolean);
325
+ const dateCreated = record.curation?.reviewedAt;
326
+ jsonLd = {
327
+ "@context": "https://schema.org",
328
+ "@type": "SoftwareSourceCode",
329
+ name,
330
+ headline: name,
331
+ description: record.description ?? undefined,
332
+ url: repoUrl || homepageUrl || undefined,
333
+ codeRepository: repoUrl || undefined,
334
+ sameAs: sameAs.length ? sameAs : undefined,
335
+ programmingLanguage: language ?? undefined,
336
+ license: licenseSpdx ?? undefined,
337
+ applicationCategory: record.category || undefined,
338
+ operatingSystem: platforms.length ? platforms.join(", ") : undefined,
339
+ keywords: record.tags?.length ? record.tags.join(", ") : undefined,
340
+ dateCreated: dateCreated ?? undefined,
341
+ dateModified: pushedAt ?? dateCreated ?? undefined,
342
+ interactionStatistic: stars > 0 ? {
343
+ "@type": "InteractionCounter",
344
+ interactionType: { "@type": "LikeAction" },
345
+ userInteractionCount: stars,
346
+ } : undefined,
347
+ author: ownerRepo ? {
348
+ "@type": "Organization",
349
+ name: ownerRepo.owner,
350
+ url: `https://github.com/${ownerRepo.owner}`,
351
+ } : undefined,
352
+ isAccessibleForFree: true,
353
+ };
354
+ } else if (record.kind === "resource") {
355
+ const schemaTypes: Record<string, string> = {
356
+ article: "Article", book: "Book", course: "Course", podcast: "PodcastSeries", video: "VideoObject",
357
+ };
358
+ jsonLd = {
359
+ "@context": "https://schema.org",
360
+ "@type": schemaTypes[record.type] ?? "CreativeWork",
361
+ name,
362
+ headline: name,
363
+ description: record.description || undefined,
364
+ url: homepageUrl || undefined,
365
+ author: record.author ? { "@type": "Person", name: record.author } : undefined,
366
+ datePublished: record.publishedAt || undefined,
367
+ keywords: record.tags?.length ? record.tags.join(", ") : undefined,
368
+ isAccessibleForFree: true,
369
+ };
370
+ } else {
371
+ jsonLd = {
372
+ "@context": "https://schema.org",
373
+ "@type": entity?.type === "person" ? "Person" : "Organization",
374
+ name,
375
+ headline: name,
376
+ description: entity?.description || undefined,
377
+ url: homepageUrl || undefined,
378
+ foundingDate: entity?.founded || undefined,
379
+ location: entity?.location || undefined,
380
+ keywords: entity?.tags?.length ? entity.tags.join(", ") : undefined,
381
+ };
382
+ }
383
+
384
+ return {
385
+ slug: routeSlug,
386
+ record,
387
+ project: proj,
388
+ entity,
389
+ isProject,
390
+ name,
391
+ title: `${name} — ${site.name}`,
392
+ description,
393
+ itemSingular: site.blueprintConfig?.labelSingular ?? record.kind,
394
+ repoUrl,
395
+ homepageUrl,
396
+ stacks,
397
+ platforms,
398
+ github,
399
+ stars,
400
+ forks,
401
+ language,
402
+ licenseSpdx,
403
+ pushedAt,
404
+ starsLabel: formatStars(stars),
405
+ pushedLabel: formatRelative(pushedAt),
406
+ ownerRepo,
407
+ avatarSrc,
408
+ initials: name.replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase(),
409
+ bestFor: proj?.bestFor ?? [],
410
+ whyListed: proj?.whyListed ?? [],
411
+ caveats: proj?.caveats ?? [],
412
+ distribution: proj?.distribution?.channels ?? [],
413
+ scores,
414
+ curationLabels: record.curation?.labels ?? [],
415
+ tags: record.tags ?? [],
416
+ healthLabel: healthStatus ? statusDisplay(healthStatus) : null,
417
+ contentHtml: isProject ? getContentHtml(recordSlug) : null,
418
+ monthlyCommits,
419
+ maxMonthlyCommits: Math.max(1, ...monthlyCommits.map((item) => item.commits)),
420
+ contributionSignals,
421
+ languages,
422
+ totalLanguageBytes: Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0)),
423
+ jsonLd,
424
+ };
425
+ }
426
+
427
+ export type DirectoryIndexModel = ReturnType<typeof getDirectoryIndexModel>;
428
+ export type DirectoryHomeModel = ReturnType<typeof getHomePageModel>;
429
+ export type SubmissionPageModel = ReturnType<typeof getSubmissionPageModel>;
430
+ export type ContributorsPageModel = ReturnType<typeof getContributorsPageModel>;
431
+ export type RecordDetailModel = NonNullable<ReturnType<typeof getRecordDetailModel>>;
432
+ export type { IndexFilters };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=theme.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.test.d.ts","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":""}
@@ -1,55 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- const astroTheme = readFileSync(resolve(import.meta.dirname, "styles.css"), "utf8");
5
- const scaffoldTheme = readFileSync(resolve(import.meta.dirname, "../templates/default/src/styles/global.css"), "utf8");
6
- const tailwindMarkup = [
7
- "components/ItemCard.astro",
8
- "layouts/Header.astro",
9
- "../templates/default/src/pages/submit.astro",
10
- ].map((file) => readFileSync(resolve(import.meta.dirname, file), "utf8")).join("\n");
11
- describe("Astro theme contract", () => {
12
- it("keeps Starlight's light and dark foundation values", () => {
13
- expect(astroTheme).toContain("--grove-background: oklch(100% 0 0)");
14
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-950)");
15
- expect(astroTheme).toContain("--grove-background: var(--color-ink-950)");
16
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-50)");
17
- expect(astroTheme).toContain("--color-ink-50: oklch(98.5% 0 0)");
18
- expect(astroTheme).toContain("--color-ink-950: oklch(14.5% 0 0)");
19
- });
20
- it("exposes runtime semantic colors to Tailwind", () => {
21
- for (const token of [
22
- "background",
23
- "foreground",
24
- "primary",
25
- "secondary",
26
- "muted",
27
- "accent",
28
- "border",
29
- "card",
30
- "popover",
31
- ]) {
32
- expect(astroTheme).toContain(`--color-${token}: var(--grove-${token})`);
33
- }
34
- });
35
- it("uses the same system and mono font stacks as the docs", () => {
36
- expect(astroTheme).toContain('--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,');
37
- expect(astroTheme).toContain("--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,");
38
- expect(astroTheme).toContain("font-family: var(--font-sans)");
39
- });
40
- it("does not duplicate package-owned foundation tokens in the scaffold", () => {
41
- expect(scaffoldTheme).not.toContain("--color-ink-");
42
- expect(scaffoldTheme).not.toContain("font-family:");
43
- expect(scaffoldTheme).not.toContain("color-scheme:");
44
- });
45
- it("styles components in HTML with Starlight-aligned Tailwind utilities", () => {
46
- expect(tailwindMarkup).toContain("rounded-[calc(var(--radius)+0.25rem)]");
47
- expect(tailwindMarkup).toContain("rounded-full");
48
- expect(tailwindMarkup).toContain("min-h-5");
49
- expect(tailwindMarkup).toContain("h-8");
50
- expect(tailwindMarkup).toContain("focus-visible:ring-3");
51
- expect(astroTheme).not.toContain(".grove-card");
52
- expect(astroTheme).not.toContain(".grove-btn");
53
- });
54
- });
55
- //# sourceMappingURL=theme.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.test.js","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":"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"}
package/src/theme.test.ts DELETED
@@ -1,67 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
-
5
- const astroTheme = readFileSync(resolve(import.meta.dirname, "styles.css"), "utf8");
6
- const scaffoldTheme = readFileSync(
7
- resolve(import.meta.dirname, "../templates/default/src/styles/global.css"),
8
- "utf8",
9
- );
10
- const tailwindMarkup = [
11
- "components/ItemCard.astro",
12
- "layouts/Header.astro",
13
- "../templates/default/src/pages/submit.astro",
14
- ].map((file) => readFileSync(resolve(import.meta.dirname, file), "utf8")).join("\n");
15
-
16
- describe("Astro theme contract", () => {
17
- it("keeps Starlight's light and dark foundation values", () => {
18
- expect(astroTheme).toContain("--grove-background: oklch(100% 0 0)");
19
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-950)");
20
- expect(astroTheme).toContain("--grove-background: var(--color-ink-950)");
21
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-50)");
22
- expect(astroTheme).toContain("--color-ink-50: oklch(98.5% 0 0)");
23
- expect(astroTheme).toContain("--color-ink-950: oklch(14.5% 0 0)");
24
- });
25
-
26
- it("exposes runtime semantic colors to Tailwind", () => {
27
- for (const token of [
28
- "background",
29
- "foreground",
30
- "primary",
31
- "secondary",
32
- "muted",
33
- "accent",
34
- "border",
35
- "card",
36
- "popover",
37
- ]) {
38
- expect(astroTheme).toContain(`--color-${token}: var(--grove-${token})`);
39
- }
40
- });
41
-
42
- it("uses the same system and mono font stacks as the docs", () => {
43
- expect(astroTheme).toContain(
44
- '--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,',
45
- );
46
- expect(astroTheme).toContain(
47
- "--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,",
48
- );
49
- expect(astroTheme).toContain("font-family: var(--font-sans)");
50
- });
51
-
52
- it("does not duplicate package-owned foundation tokens in the scaffold", () => {
53
- expect(scaffoldTheme).not.toContain("--color-ink-");
54
- expect(scaffoldTheme).not.toContain("font-family:");
55
- expect(scaffoldTheme).not.toContain("color-scheme:");
56
- });
57
-
58
- it("styles components in HTML with Starlight-aligned Tailwind utilities", () => {
59
- expect(tailwindMarkup).toContain("rounded-[calc(var(--radius)+0.25rem)]");
60
- expect(tailwindMarkup).toContain("rounded-full");
61
- expect(tailwindMarkup).toContain("min-h-5");
62
- expect(tailwindMarkup).toContain("h-8");
63
- expect(tailwindMarkup).toContain("focus-visible:ring-3");
64
- expect(astroTheme).not.toContain(".grove-card");
65
- expect(astroTheme).not.toContain(".grove-btn");
66
- });
67
- });
@@ -1,33 +0,0 @@
1
- ---
2
- name: Bug report
3
- about: Something is broken on the site
4
- title: "[bug] "
5
- labels: ["bug"]
6
- assignees: []
7
- ---
8
-
9
- ## What happened
10
-
11
- A clear, concise description of the bug.
12
-
13
- ## Steps to reproduce
14
-
15
- 1. Go to '…'
16
- 2. Click on '…'
17
- 3. Scroll down to '…'
18
- 4. See error
19
-
20
- ## What I expected
21
-
22
- A clear, concise description of what you expected to happen.
23
-
24
- ## What I saw
25
-
26
- Screenshots, error messages, console output — anything that helps.
27
-
28
- ## Environment
29
-
30
- - Browser:
31
- - OS:
32
- - Page URL:
33
- - Viewport size (rough):
@@ -1,20 +0,0 @@
1
- ---
2
- name: Feature request
3
- about: Suggest an improvement to the site
4
- title: "[feature] "
5
- labels: ["enhancement"]
6
- assignees: []
7
- ---
8
-
9
- ## The problem
10
-
11
- What's missing or awkward today?
12
-
13
- ## The proposed solution
14
-
15
- What would you like to see? Mockups, links, references — anything
16
- that helps us understand the shape of the change.
17
-
18
- ## Alternatives considered
19
-
20
- Any other approaches you weighed, and why this one wins.
@@ -1,48 +0,0 @@
1
- ---
2
- name: Submit a record
3
- about: Suggest a new entry for the directory
4
- title: "[submit] "
5
- labels: ["submission"]
6
- assignees: []
7
- ---
8
-
9
- Thanks for taking the time to suggest a new entry! Please fill in the
10
- template below — the more complete it is, the faster it lands.
11
-
12
- **Important:** this issue tracker is for **suggestions**, not for the
13
- actual data. Once approved, a maintainer will add the record to
14
- `data/records/<slug>.yml` (or invite you to open a PR).
15
-
16
- ## 1. Basics
17
-
18
- - **Name:**
19
- - **One-line description:**
20
- - **Category:** (e.g. Mobile, Web, DevOps, AI/ML)
21
- - **Primary stack:** (e.g. Flutter, Vue, Go, Python)
22
- - **Platforms:** (e.g. iOS, Android, Web, Linux, Self-host)
23
- - **Project type:** (real-app, production, reference, library, tool,
24
- demo, template, historical)
25
- - **Difficulty:** (beginner, intermediate, advanced)
26
-
27
- ## 2. Links
28
-
29
- - **Repository URL:** (the canonical `https://github.com/owner/repo`)
30
- - **Website / homepage:**
31
- - **Docs:**
32
-
33
- ## 3. Why it deserves a spot
34
-
35
- A short paragraph — what makes it stand out, what it's good for, who
36
- should look at it.
37
-
38
- ## 4. Caveats
39
-
40
- Anything we should know about: license quirks, abandoned status,
41
- paid-only features, etc.
42
-
43
- ## 5. Checkboxes
44
-
45
- - [ ] The repo is **public** on GitHub / another public forge
46
- - [ ] It's **open source** under an OSI-approved license
47
- - [ ] It has been **maintained** in the last 12 months
48
- - [ ] I am **not** the author (or I'm disclosing that I am)