@grove-dev/astro 0.2.10 → 0.2.13

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 (235) hide show
  1. package/dist/lib/display.d.ts +11 -17
  2. package/dist/lib/display.d.ts.map +1 -1
  3. package/dist/lib/display.js +43 -43
  4. package/dist/lib/display.js.map +1 -1
  5. package/dist/lib/format.d.ts +6 -7
  6. package/dist/lib/format.d.ts.map +1 -1
  7. package/dist/lib/format.js +11 -12
  8. package/dist/lib/format.js.map +1 -1
  9. package/dist/lib/lenses.d.ts +35 -73
  10. package/dist/lib/lenses.d.ts.map +1 -1
  11. package/dist/lib/lenses.js +144 -126
  12. package/dist/lib/lenses.js.map +1 -1
  13. package/dist/lib/repo.d.ts +11 -22
  14. package/dist/lib/repo.d.ts.map +1 -1
  15. package/dist/lib/repo.js +12 -25
  16. package/dist/lib/repo.js.map +1 -1
  17. package/dist/lib/scores.d.ts +15 -16
  18. package/dist/lib/scores.d.ts.map +1 -1
  19. package/dist/lib/scores.js +4 -11
  20. package/dist/lib/scores.js.map +1 -1
  21. package/dist/lib/search.d.ts +47 -129
  22. package/dist/lib/search.d.ts.map +1 -1
  23. package/dist/lib/search.js +195 -180
  24. package/dist/lib/search.js.map +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +7 -21
  26. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  27. package/dist/lib/taxonomy-counts.js +12 -31
  28. package/dist/lib/taxonomy-counts.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/components/AppsIndexRow.astro +55 -184
  31. package/src/components/AppsPagination.astro +45 -108
  32. package/src/components/CategoryGrid.astro +43 -31
  33. package/src/components/ContributorsGrid.astro +3 -3
  34. package/src/components/CurationGrid.astro +41 -0
  35. package/src/components/ExploreByStack.astro +6 -0
  36. package/src/components/FilterGroupMenu.astro +49 -0
  37. package/src/components/FilterOptions.astro +44 -0
  38. package/src/components/Hero.astro +198 -147
  39. package/src/components/Icon.astro +10 -3
  40. package/src/components/ItemCard.astro +266 -248
  41. package/src/components/RecordSection.astro +68 -33
  42. package/src/components/RefinePanel.astro +115 -141
  43. package/src/components/StackGrid.astro +32 -24
  44. package/src/components/WhyThisExists.astro +32 -32
  45. package/src/layouts/BaseLayout.astro +104 -49
  46. package/src/layouts/Container.astro +5 -44
  47. package/src/layouts/Footer.astro +31 -20
  48. package/src/layouts/Header.astro +40 -19
  49. package/src/layouts/SectionHeader.astro +27 -43
  50. package/src/layouts/Seo.astro +38 -56
  51. package/src/layouts/ThemeToggle.astro +35 -48
  52. package/src/lib/display.ts +46 -53
  53. package/src/lib/format.ts +11 -12
  54. package/src/lib/lenses.ts +151 -172
  55. package/src/lib/repo.ts +16 -28
  56. package/src/lib/scores.ts +15 -19
  57. package/src/lib/search.ts +222 -290
  58. package/src/lib/taxonomy-counts.ts +12 -46
  59. package/src/styles.css +207 -1
  60. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  61. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  62. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
  63. package/templates/default/.github/pull_request_template.md +27 -0
  64. package/templates/default/.github/workflows/build.yml +55 -0
  65. package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
  66. package/templates/default/.github/workflows/daily-refresh.yml +53 -0
  67. package/templates/default/.github/workflows/sync-contributors.yml +53 -0
  68. package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
  69. package/templates/default/.github/workflows/validate-data.yml +35 -0
  70. package/templates/default/data/records/coolify.yml +46 -0
  71. package/templates/default/data/records/gitea.yml +46 -0
  72. package/templates/default/data/records/hoppscotch.yml +47 -0
  73. package/templates/default/data/records/inventree.yml +47 -0
  74. package/templates/default/data/records/label-studio.yml +46 -0
  75. package/templates/default/data/records/logseq.yml +48 -0
  76. package/templates/default/data/records/private-gpt.yml +47 -0
  77. package/templates/default/data/records/wakapi.yml +44 -0
  78. package/templates/default/data/taxonomy/categories.yml +47 -0
  79. package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
  80. package/templates/default/data/taxonomy/platforms.yml +27 -0
  81. package/templates/default/data/taxonomy/stacks.yml +41 -0
  82. package/templates/default/grove.config.ts +1 -1
  83. package/templates/default/package.json +5 -4
  84. package/templates/default/public/icons/stacks/bun.svg +1 -0
  85. package/templates/default/public/icons/stacks/clojure.svg +1 -0
  86. package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
  87. package/templates/default/public/icons/stacks/deno.svg +1 -0
  88. package/templates/default/public/icons/stacks/django.svg +1 -0
  89. package/templates/default/public/icons/stacks/docker.svg +1 -0
  90. package/templates/default/public/icons/stacks/go.svg +1 -0
  91. package/templates/default/public/icons/stacks/llm.svg +1 -0
  92. package/templates/default/public/icons/stacks/node.js.svg +1 -0
  93. package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
  94. package/templates/default/public/icons/stacks/vue.svg +1 -0
  95. package/templates/default/public/llms-full.txt +197 -5
  96. package/templates/default/public/llms.txt +107 -6
  97. package/templates/default/public/sitemap.xml +63 -0
  98. package/templates/default/scripts/build-llms.mjs +123 -0
  99. package/templates/default/scripts/build-records-json.mjs +27 -0
  100. package/templates/default/scripts/build-sitemap.mjs +20 -0
  101. package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
  102. package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
  103. package/templates/default/scripts/fetch-icons.mjs +137 -0
  104. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
  105. package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
  106. package/templates/default/scripts/refresh-records-activity.mjs +24 -0
  107. package/templates/default/scripts/repair-license-format.mjs +72 -0
  108. package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
  109. package/templates/default/scripts/seed-from-github.mjs +62 -0
  110. package/templates/default/scripts/sync-contributors.mjs +145 -0
  111. package/templates/default/scripts/sync-github-metadata.mjs +28 -0
  112. package/templates/default/scripts/validate-records.mjs +27 -0
  113. package/templates/default/src/data/records.ts +134 -51
  114. package/templates/default/src/pages/404.astro +64 -0
  115. package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
  116. package/templates/default/src/pages/[slug]/index.astro +476 -0
  117. package/templates/default/src/pages/about.astro +165 -82
  118. package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
  119. package/templates/default/src/pages/apps/index.astro +146 -0
  120. package/templates/default/src/pages/contributors.astro +184 -0
  121. package/templates/default/src/pages/index.astro +119 -162
  122. package/templates/default/src/pages/submit.astro +276 -114
  123. package/templates/default/wrangler.jsonc +28 -0
  124. package/LICENSE +0 -21
  125. package/templates/default/.astro/content.d.ts +0 -155
  126. package/templates/default/.astro/settings.json +0 -5
  127. package/templates/default/.astro/types.d.ts +0 -1
  128. package/templates/default/data/generated/apps.json +0 -1
  129. package/templates/default/data/generated/records.full.json +0 -8
  130. package/templates/default/data/generated/records.index.json +0 -7
  131. package/templates/default/data/generated/records.json +0 -8
  132. package/templates/default/data/generated/repo-stats.json +0 -1
  133. package/templates/default/data/generated/site-config.json +0 -31
  134. package/templates/default/dist/_astro/BaseLayout.Dz_HR-68.css +0 -1
  135. package/templates/default/dist/about/index.html +0 -165
  136. package/templates/default/dist/icons/platforms/android.svg +0 -1
  137. package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
  138. package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
  139. package/templates/default/dist/icons/platforms/chrome.svg +0 -1
  140. package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
  141. package/templates/default/dist/icons/platforms/desktop.svg +0 -4
  142. package/templates/default/dist/icons/platforms/embedded.svg +0 -5
  143. package/templates/default/dist/icons/platforms/ios.svg +0 -1
  144. package/templates/default/dist/icons/platforms/linux.svg +0 -1
  145. package/templates/default/dist/icons/platforms/macos.svg +0 -1
  146. package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
  147. package/templates/default/dist/icons/platforms/web.svg +0 -5
  148. package/templates/default/dist/icons/platforms/windows.svg +0 -6
  149. package/templates/default/dist/icons/stacks/android.svg +0 -1
  150. package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
  151. package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
  152. package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
  153. package/templates/default/dist/icons/stacks/dart.svg +0 -1
  154. package/templates/default/dist/icons/stacks/firebase.svg +0 -1
  155. package/templates/default/dist/icons/stacks/flutter.svg +0 -1
  156. package/templates/default/dist/icons/stacks/graphql.svg +0 -1
  157. package/templates/default/dist/icons/stacks/ionic.svg +0 -4
  158. package/templates/default/dist/icons/stacks/java.svg +0 -1
  159. package/templates/default/dist/icons/stacks/javascript.svg +0 -1
  160. package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
  161. package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
  162. package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
  163. package/templates/default/dist/icons/stacks/python.svg +0 -1
  164. package/templates/default/dist/icons/stacks/react-native.svg +0 -1
  165. package/templates/default/dist/icons/stacks/react.svg +0 -1
  166. package/templates/default/dist/icons/stacks/rust.svg +0 -1
  167. package/templates/default/dist/icons/stacks/solidity.svg +0 -1
  168. package/templates/default/dist/icons/stacks/swift.svg +0 -1
  169. package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
  170. package/templates/default/dist/icons/stacks/typescript.svg +0 -1
  171. package/templates/default/dist/index.html +0 -55
  172. package/templates/default/dist/llms-full.txt +0 -14
  173. package/templates/default/dist/llms.txt +0 -11
  174. package/templates/default/dist/og-image.svg +0 -48
  175. package/templates/default/dist/projects/index.html +0 -61
  176. package/templates/default/dist/robots.txt +0 -53
  177. package/templates/default/dist/sitemap.xml +0 -21
  178. package/templates/default/dist/submit/index.html +0 -69
  179. package/templates/default/node_modules/.bin/astro +0 -21
  180. package/templates/default/node_modules/.bin/astro-check +0 -21
  181. package/templates/default/node_modules/.bin/grove +0 -21
  182. package/templates/default/node_modules/.bin/jiti +0 -21
  183. package/templates/default/node_modules/.bin/prettier +0 -21
  184. package/templates/default/node_modules/.bin/rollup +0 -21
  185. package/templates/default/node_modules/.bin/tsc +0 -21
  186. package/templates/default/node_modules/.bin/tsserver +0 -21
  187. package/templates/default/node_modules/.bin/tsx +0 -21
  188. package/templates/default/node_modules/.bin/vite +0 -21
  189. package/templates/default/node_modules/.bin/yaml +0 -21
  190. package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
  191. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
  192. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
  193. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
  194. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
  195. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
  196. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
  197. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
  198. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
  199. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
  200. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
  201. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
  202. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
  203. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
  204. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
  205. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
  206. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
  207. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
  208. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
  209. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
  210. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
  211. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
  212. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
  213. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
  214. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
  215. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
  216. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
  217. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
  218. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
  219. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
  220. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
  221. package/templates/default/node_modules/.vite/deps/package.json +0 -3
  222. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
  223. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
  224. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
  225. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
  226. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
  227. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
  228. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
  229. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +0 -7
  230. package/templates/default/src/components/layout/BaseLayout.astro +0 -47
  231. package/templates/default/src/components/layout/Container.astro +0 -18
  232. package/templates/default/src/components/layout/Footer.astro +0 -27
  233. package/templates/default/src/components/layout/Header.astro +0 -43
  234. package/templates/default/src/lib/markdown.ts +0 -171
  235. package/templates/default/src/pages/projects/index.astro +0 -193
@@ -0,0 +1,146 @@
1
+ ---
2
+ /**
3
+ * /apps — alternate list URL.
4
+ *
5
+ * Mirrors `/[slug]/index.astro` (the blueprint-aware dynamic list
6
+ * page) at a static URL so the openapps-style `/apps` route works
7
+ * for project-directory blueprints without per-blueprint forks.
8
+ *
9
+ * For non-project blueprints (resource-hub → /resources,
10
+ * ecosystem-map → /entities), this page redirects to the matching
11
+ * dynamic route.
12
+ */
13
+ import { indexSlug, itemLabelPlural, items } from "../../data/records";
14
+ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
15
+ import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
16
+ import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
17
+ import AppsIndexRow from "@grove-dev/astro/components/AppsIndexRow.astro";
18
+ import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
19
+ import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
20
+ import siteConfig from "../../../data/generated/site-config.json";
21
+ import {
22
+ activeFilterChips,
23
+ applySort,
24
+ buildFacets,
25
+ effectivePage,
26
+ effectiveSort,
27
+ filterApps,
28
+ filtersFromSearchParams,
29
+ paginate,
30
+ totalPages,
31
+ } from "@grove-dev/astro";
32
+
33
+ const slug = indexSlug();
34
+
35
+ // Only render the static /apps route when the active blueprint is
36
+ // project-directory (the openapps-style URL). For other blueprints,
37
+ // redirect to the dynamic `/<slug>/` route.
38
+ if (slug !== "projects") {
39
+ return Astro.redirect(`/${slug}${Astro.url.search}`);
40
+ }
41
+
42
+ const total = items.length;
43
+ const filters = filtersFromSearchParams(Astro.url.searchParams);
44
+ const facets = buildFacets(items);
45
+ const sort = effectiveSort(filters);
46
+ const requestedPage = effectivePage(filters);
47
+ const filtered = filterApps(items, filters);
48
+ const sorted = applySort(filtered, sort);
49
+ const pages = totalPages(sorted.length);
50
+ const page = Math.min(requestedPage, pages);
51
+ const visibleItems = paginate(sorted, page);
52
+ const chips = activeFilterChips(filters);
53
+ const itemPlural = itemLabelPlural();
54
+
55
+ const title = `${itemPlural.charAt(0).toUpperCase()}${itemPlural.slice(1)} — ${siteConfig.name}`;
56
+ const description = `Browse every curated ${itemPlural.replace(/s$/, "")} in the ${siteConfig.name} directory.`;
57
+ ---
58
+
59
+ <BaseLayout title={title} description={description} site={siteConfig}>
60
+ <section class="grove-section">
61
+ <div class="grove-container-wide mx-auto px-5 sm:px-7">
62
+ <header class="mb-6">
63
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
64
+ {itemPlural}
65
+ </span>
66
+ <h1 class="m-0 mt-1 text-[1.75rem] sm:text-[2.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
67
+ Browse the directory
68
+ </h1>
69
+ <p class="m-0 mt-2 max-w-2xl text-base leading-relaxed text-ink-700 dark:text-ink-200">
70
+ Search and filter every curated {itemPlural.replace(/s$/, "")}. Use the
71
+ lens tabs to focus on a curated angle, or refine by stack,
72
+ platform, license, and status.
73
+ </p>
74
+ </header>
75
+
76
+ <div class="mb-6">
77
+ <SmartLensTabs
78
+ currentParams={Astro.url.searchParams}
79
+ pathPrefix="/apps"
80
+ />
81
+ </div>
82
+
83
+ <div class="mb-8">
84
+ <RefinePanel
85
+ facets={facets}
86
+ initial={filters}
87
+ pathPrefix="/apps"
88
+ />
89
+ </div>
90
+
91
+ {chips.length > 0 && (
92
+ <ul class="mb-6 flex flex-wrap items-center gap-1.5" aria-label="Active filters">
93
+ {chips.map((chip) => (
94
+ <li>
95
+ <a
96
+ href={chip.removeHref}
97
+ class="grove-badge inline-flex items-center gap-1.5 hover:border-ink-300 dark:hover:border-ink-700"
98
+ title={`Remove filter: ${chip.label}`}
99
+ >
100
+ <span>{chip.label}</span>
101
+ <span aria-hidden="true">×</span>
102
+ </a>
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ )}
107
+
108
+ {visibleItems.length === 0 ? (
109
+ <p class="grove-card p-8 text-center text-sm text-ink-500 dark:text-ink-400">
110
+ No {itemPlural} match these filters. Try clearing a filter or two.
111
+ </p>
112
+ ) : (
113
+ <ul class="m-0 grid list-none grid-cols-1 gap-3 p-0">
114
+ {visibleItems.map((item) => (
115
+ <li class="m-0 p-0">
116
+ <AppsIndexRow record={item as Parameters<typeof AppsIndexRow>[0]["record"]} detailHref={`/apps/${item.slug}`} />
117
+ </li>
118
+ ))}
119
+ </ul>
120
+ )}
121
+ </div>
122
+ </section>
123
+
124
+ <ExploreByCategory
125
+ categories={[
126
+ ...new Set(
127
+ items
128
+ .filter((i) => i.kind === "project")
129
+ .map((i) => (i as { category?: string }).category ?? "")
130
+ .filter(Boolean),
131
+ ),
132
+ ].map((name) => ({ name, slug: name }))}
133
+ pathPrefix="/apps"
134
+ />
135
+ <ExploreByStack
136
+ stacks={[
137
+ ...new Set(
138
+ items
139
+ .filter((i) => i.kind === "project")
140
+ .flatMap((i) => [(i as { stack?: string }).stack ?? "", ...(i as { stacks?: string[] }).stacks])
141
+ .filter(Boolean),
142
+ ),
143
+ ].map((name) => ({ name, slug: name }))}
144
+ pathPrefix="/apps"
145
+ />
146
+ </BaseLayout>
@@ -0,0 +1,184 @@
1
+ ---
2
+ /**
3
+ * Contributors page — full list with per-user contribution counts.
4
+ *
5
+ * Generic: works for any blueprint (project-directory, resource-hub,
6
+ * ecosystem-map) and any source of contributors. Reads from
7
+ * `data/generated/contributors.json` if present, otherwise renders
8
+ * a graceful empty state. The file is produced by the
9
+ * `sync-contributors` GitHub Action (see `.github/workflows/`).
10
+ *
11
+ * Falls back to deriving contributors from the records themselves
12
+ * (record.github.fullName → owner handle) when no JSON file exists,
13
+ * so a fresh scaffold still shows something useful.
14
+ */
15
+ import siteConfig from "../../data/generated/site-config.json";
16
+ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
17
+ import { projects } from "../data/records";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { fileURLToPath } from "node:url";
20
+ import { dirname, resolve } from "node:path";
21
+
22
+ interface Contributor {
23
+ username: string;
24
+ name?: string;
25
+ avatarUrl?: string;
26
+ profileUrl?: string;
27
+ contributions?: number;
28
+ }
29
+
30
+ const here = dirname(fileURLToPath(import.meta.url));
31
+ const contributorsPath = [
32
+ resolve(here, "..", "..", "data", "generated", "contributors.json"),
33
+ resolve(process.cwd(), "data", "generated", "contributors.json"),
34
+ ].find((p) => existsSync(p));
35
+
36
+ let contributors: Contributor[] = [];
37
+ if (contributorsPath) {
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(contributorsPath, "utf8")) as {
40
+ contributors?: Contributor[];
41
+ };
42
+ contributors = parsed.contributors ?? [];
43
+ } catch {
44
+ contributors = [];
45
+ }
46
+ }
47
+
48
+ // Fallback: derive owners from record.github.fullName when no JSON
49
+ // is present (fresh scaffold before the first sync run).
50
+ if (contributors.length === 0) {
51
+ const seen = new Map<string, Contributor>();
52
+ for (const r of projects) {
53
+ const full = r.github?.fullName;
54
+ if (!full || !full.includes("/")) continue;
55
+ const username = full.split("/")[0];
56
+ if (seen.has(username)) continue;
57
+ seen.set(username, {
58
+ username,
59
+ avatarUrl: `https://avatars.githubusercontent.com/${username}?v=4&s=80`,
60
+ profileUrl: `https://github.com/${username}`,
61
+ });
62
+ }
63
+ contributors = [...seen.values()].sort((a, b) =>
64
+ a.username.localeCompare(b.username),
65
+ );
66
+ }
67
+
68
+ const total = contributors.length;
69
+ const sorted = [...contributors].sort((a, b) => {
70
+ const ac = a.contributions ?? 0;
71
+ const bc = b.contributions ?? 0;
72
+ if (bc !== ac) return bc - ac;
73
+ return a.username.localeCompare(b.username);
74
+ });
75
+
76
+ const title = `Contributors — ${siteConfig.name}`;
77
+ const description =
78
+ `Every person who has helped build ${siteConfig.name} — from a single line to long-running stewardship.`;
79
+
80
+ const repoUrl = siteConfig.repoUrl;
81
+ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/contributors` : null;
82
+ ---
83
+
84
+ <BaseLayout title={title} description={description} site={siteConfig}>
85
+ <section class="grove-section">
86
+ <div class="grove-container-wide mx-auto px-5 py-12 sm:px-7 sm:py-16">
87
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
88
+ Contributors
89
+ </span>
90
+ <h1 class="m-0 mt-2 text-[2rem] sm:text-[2.5rem] font-semibold leading-[1.05] tracking-tight text-ink-900 dark:text-ink-100">
91
+ {total > 0 ? `Showing all ${total} contributors` : "Contributors"}
92
+ </h1>
93
+ <p class="m-0 mt-3 max-w-2xl text-base leading-relaxed text-ink-700 dark:text-ink-200">
94
+ {total > 0
95
+ ? `Every person who has helped build ${siteConfig.name} — from a single line to long-running stewardship.`
96
+ : `Contributors will appear here once ${siteConfig.name} syncs its first set of GitHub metadata. The list is auto-generated from ${repoUrl ? repoUrl : "the project's source repository"}.`}
97
+ </p>
98
+
99
+ {contributorsGraphHref && (
100
+ <p class="m-0 mt-3 text-sm text-ink-500 dark:text-ink-400">
101
+ <a
102
+ href={contributorsGraphHref}
103
+ target="_blank"
104
+ rel="noopener noreferrer"
105
+ class="font-medium underline decoration-ink-300 underline-offset-2 hover:decoration-ink-500 dark:decoration-ink-700 dark:hover:decoration-ink-400"
106
+ >
107
+ GitHub contributors graph →
108
+ </a>
109
+ </p>
110
+ )}
111
+ </div>
112
+ </section>
113
+
114
+ {sorted.length === 0 ? (
115
+ <section class="grove-section">
116
+ <div class="grove-container mx-auto px-5 pb-16 sm:px-7">
117
+ <div class="grove-card flex flex-col items-center gap-3 p-8 text-center">
118
+ <p class="m-0 text-sm text-ink-500 dark:text-ink-400">
119
+ No contributors synced yet. Once the first sync runs, every
120
+ contributor avatar lands here.
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </section>
125
+ ) : (
126
+ <section class="grove-section" aria-label="Contributors list">
127
+ <div class="grove-container-wide mx-auto px-5 pb-16 sm:px-7">
128
+ <ul
129
+ class="m-0 grid list-none grid-cols-2 gap-3 p-0 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
130
+ aria-label="Contributors"
131
+ >
132
+ {sorted.map((c) => {
133
+ const href = c.profileUrl ?? `https://github.com/${c.username}`;
134
+ const avatar =
135
+ c.avatarUrl ??
136
+ `https://avatars.githubusercontent.com/${c.username}?v=4&s=80`;
137
+ return (
138
+ <li class="m-0 p-0">
139
+ <a
140
+ href={href}
141
+ target="_blank"
142
+ rel="noopener noreferrer"
143
+ class="group grove-card flex flex-col items-center gap-2 p-3 text-center no-underline transition-colors hover:border-ink-300 dark:hover:border-ink-700"
144
+ data-event="contributor_click"
145
+ data-event-source="contributors_page"
146
+ >
147
+ <img
148
+ src={avatar}
149
+ alt={c.name ?? c.username}
150
+ width="56"
151
+ height="56"
152
+ loading="lazy"
153
+ decoding="async"
154
+ class="h-14 w-14 rounded-full border border-ink-200 object-cover dark:border-ink-800"
155
+ />
156
+ <span class="line-clamp-1 text-sm font-medium text-ink-700 group-hover:text-ink-900 dark:text-ink-300 dark:group-hover:text-ink-100">
157
+ {c.name ?? c.username}
158
+ </span>
159
+ <span class="text-2xs text-ink-500 dark:text-ink-400">
160
+ @{c.username}
161
+ </span>
162
+ {typeof c.contributions === "number" && (
163
+ <span class="font-mono text-2xs text-ink-400">
164
+ {c.contributions.toLocaleString()} contributions
165
+ </span>
166
+ )}
167
+ </a>
168
+ </li>
169
+ );
170
+ })}
171
+ </ul>
172
+ </div>
173
+ </section>
174
+ )}
175
+ </BaseLayout>
176
+
177
+ <style>
178
+ .line-clamp-1 {
179
+ display: -webkit-box;
180
+ -webkit-line-clamp: 1;
181
+ -webkit-box-orient: vertical;
182
+ overflow: hidden;
183
+ }
184
+ </style>