@grove-dev/astro 0.2.10 → 0.2.14

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 (237) hide show
  1. package/README.md +26 -18
  2. package/dist/lib/display.d.ts +11 -17
  3. package/dist/lib/display.d.ts.map +1 -1
  4. package/dist/lib/display.js +43 -43
  5. package/dist/lib/display.js.map +1 -1
  6. package/dist/lib/format.d.ts +6 -7
  7. package/dist/lib/format.d.ts.map +1 -1
  8. package/dist/lib/format.js +11 -12
  9. package/dist/lib/format.js.map +1 -1
  10. package/dist/lib/lenses.d.ts +35 -73
  11. package/dist/lib/lenses.d.ts.map +1 -1
  12. package/dist/lib/lenses.js +144 -126
  13. package/dist/lib/lenses.js.map +1 -1
  14. package/dist/lib/repo.d.ts +12 -22
  15. package/dist/lib/repo.d.ts.map +1 -1
  16. package/dist/lib/repo.js +13 -25
  17. package/dist/lib/repo.js.map +1 -1
  18. package/dist/lib/scores.d.ts +15 -16
  19. package/dist/lib/scores.d.ts.map +1 -1
  20. package/dist/lib/scores.js +4 -11
  21. package/dist/lib/scores.js.map +1 -1
  22. package/dist/lib/search.d.ts +44 -129
  23. package/dist/lib/search.d.ts.map +1 -1
  24. package/dist/lib/search.js +192 -180
  25. package/dist/lib/search.js.map +1 -1
  26. package/dist/lib/taxonomy-counts.d.ts +7 -21
  27. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  28. package/dist/lib/taxonomy-counts.js +12 -31
  29. package/dist/lib/taxonomy-counts.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/components/CategoryGrid.astro +43 -31
  32. package/src/components/ContributorsGrid.astro +3 -3
  33. package/src/components/CurationGrid.astro +41 -0
  34. package/src/components/ExploreByStack.astro +6 -0
  35. package/src/components/FilterGroupMenu.astro +49 -0
  36. package/src/components/FilterOptions.astro +44 -0
  37. package/src/components/Hero.astro +198 -147
  38. package/src/components/Icon.astro +10 -3
  39. package/src/components/IndexRow.astro +75 -0
  40. package/src/components/ItemCard.astro +266 -248
  41. package/src/components/Pagination.astro +63 -0
  42. package/src/components/RecordSection.astro +70 -34
  43. package/src/components/RefinePanel.astro +115 -141
  44. package/src/components/StackGrid.astro +32 -24
  45. package/src/components/WhyThisExists.astro +32 -32
  46. package/src/layouts/BaseLayout.astro +104 -49
  47. package/src/layouts/Container.astro +5 -44
  48. package/src/layouts/Footer.astro +31 -20
  49. package/src/layouts/Header.astro +40 -19
  50. package/src/layouts/SectionHeader.astro +27 -43
  51. package/src/layouts/Seo.astro +38 -56
  52. package/src/layouts/ThemeToggle.astro +35 -48
  53. package/src/lib/display.ts +46 -53
  54. package/src/lib/format.ts +11 -12
  55. package/src/lib/lenses.ts +151 -172
  56. package/src/lib/repo.ts +17 -28
  57. package/src/lib/scores.ts +15 -19
  58. package/src/lib/search.ts +218 -291
  59. package/src/lib/taxonomy-counts.ts +12 -46
  60. package/src/styles.css +209 -1
  61. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  62. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  63. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
  64. package/templates/default/.github/pull_request_template.md +27 -0
  65. package/templates/default/.github/workflows/build.yml +55 -0
  66. package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
  67. package/templates/default/.github/workflows/daily-refresh.yml +53 -0
  68. package/templates/default/.github/workflows/sync-contributors.yml +53 -0
  69. package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
  70. package/templates/default/.github/workflows/validate-data.yml +35 -0
  71. package/templates/default/data/records/coolify.yml +46 -0
  72. package/templates/default/data/records/gitea.yml +46 -0
  73. package/templates/default/data/records/hoppscotch.yml +47 -0
  74. package/templates/default/data/records/inventree.yml +47 -0
  75. package/templates/default/data/records/label-studio.yml +46 -0
  76. package/templates/default/data/records/logseq.yml +48 -0
  77. package/templates/default/data/records/private-gpt.yml +47 -0
  78. package/templates/default/data/records/wakapi.yml +44 -0
  79. package/templates/default/data/taxonomy/categories.yml +47 -0
  80. package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
  81. package/templates/default/data/taxonomy/platforms.yml +27 -0
  82. package/templates/default/data/taxonomy/stacks.yml +41 -0
  83. package/templates/default/grove.config.ts +1 -1
  84. package/templates/default/package.json +5 -4
  85. package/templates/default/public/icons/stacks/bun.svg +1 -0
  86. package/templates/default/public/icons/stacks/clojure.svg +1 -0
  87. package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
  88. package/templates/default/public/icons/stacks/deno.svg +1 -0
  89. package/templates/default/public/icons/stacks/django.svg +1 -0
  90. package/templates/default/public/icons/stacks/docker.svg +1 -0
  91. package/templates/default/public/icons/stacks/go.svg +1 -0
  92. package/templates/default/public/icons/stacks/llm.svg +1 -0
  93. package/templates/default/public/icons/stacks/node.js.svg +1 -0
  94. package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
  95. package/templates/default/public/icons/stacks/vue.svg +1 -0
  96. package/templates/default/public/llms-full.txt +197 -5
  97. package/templates/default/public/llms.txt +107 -6
  98. package/templates/default/public/sitemap.xml +63 -0
  99. package/templates/default/scripts/build-llms.mjs +123 -0
  100. package/templates/default/scripts/build-records-json.mjs +27 -0
  101. package/templates/default/scripts/build-sitemap.mjs +20 -0
  102. package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
  103. package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
  104. package/templates/default/scripts/fetch-icons.mjs +137 -0
  105. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
  106. package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
  107. package/templates/default/scripts/refresh-records-activity.mjs +24 -0
  108. package/templates/default/scripts/repair-license-format.mjs +72 -0
  109. package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
  110. package/templates/default/scripts/seed-from-github.mjs +62 -0
  111. package/templates/default/scripts/sync-contributors.mjs +145 -0
  112. package/templates/default/scripts/sync-github-metadata.mjs +28 -0
  113. package/templates/default/scripts/validate-records.mjs +27 -0
  114. package/templates/default/src/data/records.ts +138 -51
  115. package/templates/default/src/pages/404.astro +64 -0
  116. package/templates/default/src/pages/{projects/[slug].astro → [slug]/[recordSlug].astro} +236 -66
  117. package/templates/default/src/pages/[slug]/index.astro +477 -0
  118. package/templates/default/src/pages/about.astro +165 -82
  119. package/templates/default/src/pages/apps/[recordSlug].astro +33 -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/src/components/AppsIndexRow.astro +0 -204
  126. package/src/components/AppsPagination.astro +0 -126
  127. package/templates/default/.astro/content.d.ts +0 -155
  128. package/templates/default/.astro/settings.json +0 -5
  129. package/templates/default/.astro/types.d.ts +0 -1
  130. package/templates/default/data/generated/apps.json +0 -1
  131. package/templates/default/data/generated/records.full.json +0 -8
  132. package/templates/default/data/generated/records.index.json +0 -7
  133. package/templates/default/data/generated/records.json +0 -8
  134. package/templates/default/data/generated/repo-stats.json +0 -1
  135. package/templates/default/data/generated/site-config.json +0 -31
  136. package/templates/default/dist/_astro/BaseLayout.Dz_HR-68.css +0 -1
  137. package/templates/default/dist/about/index.html +0 -165
  138. package/templates/default/dist/icons/platforms/android.svg +0 -1
  139. package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
  140. package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
  141. package/templates/default/dist/icons/platforms/chrome.svg +0 -1
  142. package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
  143. package/templates/default/dist/icons/platforms/desktop.svg +0 -4
  144. package/templates/default/dist/icons/platforms/embedded.svg +0 -5
  145. package/templates/default/dist/icons/platforms/ios.svg +0 -1
  146. package/templates/default/dist/icons/platforms/linux.svg +0 -1
  147. package/templates/default/dist/icons/platforms/macos.svg +0 -1
  148. package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
  149. package/templates/default/dist/icons/platforms/web.svg +0 -5
  150. package/templates/default/dist/icons/platforms/windows.svg +0 -6
  151. package/templates/default/dist/icons/stacks/android.svg +0 -1
  152. package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
  153. package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
  154. package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
  155. package/templates/default/dist/icons/stacks/dart.svg +0 -1
  156. package/templates/default/dist/icons/stacks/firebase.svg +0 -1
  157. package/templates/default/dist/icons/stacks/flutter.svg +0 -1
  158. package/templates/default/dist/icons/stacks/graphql.svg +0 -1
  159. package/templates/default/dist/icons/stacks/ionic.svg +0 -4
  160. package/templates/default/dist/icons/stacks/java.svg +0 -1
  161. package/templates/default/dist/icons/stacks/javascript.svg +0 -1
  162. package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
  163. package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
  164. package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
  165. package/templates/default/dist/icons/stacks/python.svg +0 -1
  166. package/templates/default/dist/icons/stacks/react-native.svg +0 -1
  167. package/templates/default/dist/icons/stacks/react.svg +0 -1
  168. package/templates/default/dist/icons/stacks/rust.svg +0 -1
  169. package/templates/default/dist/icons/stacks/solidity.svg +0 -1
  170. package/templates/default/dist/icons/stacks/swift.svg +0 -1
  171. package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
  172. package/templates/default/dist/icons/stacks/typescript.svg +0 -1
  173. package/templates/default/dist/index.html +0 -55
  174. package/templates/default/dist/llms-full.txt +0 -14
  175. package/templates/default/dist/llms.txt +0 -11
  176. package/templates/default/dist/og-image.svg +0 -48
  177. package/templates/default/dist/projects/index.html +0 -61
  178. package/templates/default/dist/robots.txt +0 -53
  179. package/templates/default/dist/sitemap.xml +0 -21
  180. package/templates/default/dist/submit/index.html +0 -69
  181. package/templates/default/node_modules/.bin/astro +0 -21
  182. package/templates/default/node_modules/.bin/astro-check +0 -21
  183. package/templates/default/node_modules/.bin/grove +0 -21
  184. package/templates/default/node_modules/.bin/jiti +0 -21
  185. package/templates/default/node_modules/.bin/prettier +0 -21
  186. package/templates/default/node_modules/.bin/rollup +0 -21
  187. package/templates/default/node_modules/.bin/tsc +0 -21
  188. package/templates/default/node_modules/.bin/tsserver +0 -21
  189. package/templates/default/node_modules/.bin/tsx +0 -21
  190. package/templates/default/node_modules/.bin/vite +0 -21
  191. package/templates/default/node_modules/.bin/yaml +0 -21
  192. package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
  193. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
  194. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
  195. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
  196. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
  197. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
  198. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
  199. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
  200. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
  201. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
  202. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
  203. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
  204. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
  205. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
  206. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
  207. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
  208. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
  209. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
  210. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
  211. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
  212. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
  213. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
  214. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
  215. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
  216. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
  217. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
  218. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
  219. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
  220. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
  221. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
  222. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
  223. package/templates/default/node_modules/.vite/deps/package.json +0 -3
  224. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
  225. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
  226. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
  227. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
  228. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
  229. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
  230. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
  231. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +0 -7
  232. package/templates/default/src/components/layout/BaseLayout.astro +0 -47
  233. package/templates/default/src/components/layout/Container.astro +0 -18
  234. package/templates/default/src/components/layout/Footer.astro +0 -27
  235. package/templates/default/src/components/layout/Header.astro +0 -43
  236. package/templates/default/src/lib/markdown.ts +0 -171
  237. package/templates/default/src/pages/projects/index.astro +0 -193
@@ -3,7 +3,7 @@
3
3
  * WhyThisExists — the "what is this site for?" section.
4
4
  *
5
5
  * Renders 3 short, opinionated points (icon + title + body). The
6
- * icon set is small and generic: "app" (window/grid glyph),
6
+ * icon set is small and generic: "item" (window/grid glyph),
7
7
  * "code" (angle brackets), "search" (magnifier). Defaults to
8
8
  * the canonical "directory, source, search" trio.
9
9
  *
@@ -14,11 +14,12 @@
14
14
  interface WhyPoint {
15
15
  title: string;
16
16
  body: string;
17
- icon: "app" | "code" | "search";
17
+ icon: "item" | "code" | "search";
18
18
  }
19
19
 
20
20
  interface Props {
21
21
  points?: WhyPoint[];
22
+ eyebrow?: string;
22
23
  title?: string;
23
24
  description?: string;
24
25
  class?: string;
@@ -26,26 +27,27 @@ interface Props {
26
27
 
27
28
  const {
28
29
  points,
29
- title = "Why this directory exists",
30
- description = "A short note on the point of all this.",
30
+ eyebrow = "Why this exists",
31
+ title = "Most directory discovery is fragmented.",
32
+ description = "Lists are useful, but they often become long, static pages without enough context to compare what is real, maintained, and worth exploring.",
31
33
  class: className = "",
32
34
  } = Astro.props;
33
35
 
34
36
  const defaultPoints: WhyPoint[] = [
35
37
  {
36
- icon: "app",
37
- title: "A curated starting point",
38
- body: "Skip the rabbit holes. Every entry here is something a real developer reached for, vetted for quality and longevity.",
38
+ icon: "item",
39
+ title: "Real projects",
40
+ body: "Not SDKs, templates, or tiny demos. Focused on complete projects you can actually run.",
39
41
  },
40
42
  {
41
43
  icon: "code",
42
- title: "Real, runnable source",
43
- body: "Every project links back to its source. Skim, fork, learn no paywalls, no broken download links.",
44
+ title: "Codebase context",
45
+ body: "Stack, platform, license, activity, maturity, and contribution signals in one place.",
44
46
  },
45
47
  {
46
48
  icon: "search",
47
- title: "Search that respects you",
48
- body: "Filter by stack, platform, license, or status. No SEO spam, no affiliate farms. The directory is the product.",
49
+ title: "Searchable discovery",
50
+ body: "Browse by category, stack, platform, stars, recency, and status instead of scanning a flat list.",
49
51
  },
50
52
  ];
51
53
 
@@ -53,35 +55,39 @@ const items = points && points.length > 0 ? points : defaultPoints;
53
55
  ---
54
56
 
55
57
  <section
56
- class:list={["grove-section", className]}
58
+ id="about"
59
+ class:list={["grove-section scroll-mt-20 border-b border-ink-200 dark:border-ink-800", className]}
57
60
  aria-labelledby="grove-why-heading"
58
61
  >
59
62
  <div class="grove-container mx-auto px-5 sm:px-7">
60
- <header class="mb-8 flex flex-col items-center gap-2 text-center">
63
+ <header class="flex flex-col gap-2">
64
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
65
+ {eyebrow}
66
+ </span>
61
67
  <h2
62
68
  id="grove-why-heading"
63
- class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
69
+ class="m-0 max-w-[40ch] text-[1.5rem] sm:text-[1.75rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
64
70
  >
65
71
  {title}
66
72
  </h2>
67
73
  {description && (
68
- <p class="m-0 max-w-2xl text-[0.9375rem] text-ink-500 dark:text-ink-400">
74
+ <p class="m-0 max-w-[64ch] text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400">
69
75
  {description}
70
76
  </p>
71
77
  )}
72
78
  </header>
73
79
 
74
- <ol
75
- class="m-0 grid list-none grid-cols-1 gap-6 p-0 md:grid-cols-3"
80
+ <ul
81
+ class="m-0 mt-7 grid list-none grid-cols-1 gap-3 p-0 sm:grid-cols-3"
76
82
  aria-label="Why this directory"
77
83
  >
78
- {items.map((p, i) => (
79
- <li class="flex flex-col items-center gap-2 text-center">
80
- <span
84
+ {items.map((p) => (
85
+ <li class="grove-card p-4">
86
+ <div
81
87
  aria-hidden="true"
82
- class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200"
88
+ class="mb-3 flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300"
83
89
  >
84
- {p.icon === "app" && (
90
+ {p.icon === "item" && (
85
91
  <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
86
92
  <path d="M1.75 3.5a.25.25 0 0 1 .25-.25h12a.25.25 0 0 1 .25.25v8a.25.25 0 0 1-.25.25h-12a.25.25 0 0 1-.25-.25v-8ZM2 4.75v6h12v-6H2ZM2.5 6.5h4v2.5h-4V6.5Z"></path>
87
93
  </svg>
@@ -96,21 +102,15 @@ const items = points && points.length > 0 ? points : defaultPoints;
96
102
  <path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"></path>
97
103
  </svg>
98
104
  )}
99
- </span>
100
- <span
101
- aria-hidden="true"
102
- class="font-mono text-2xs text-ink-400 dark:text-ink-500"
103
- >
104
- 0{i + 1}
105
- </span>
106
- <h3 class="m-0 text-base font-semibold tracking-tight text-ink-900 dark:text-ink-100">
105
+ </div>
106
+ <h3 class="m-0 text-[0.9375rem] font-semibold text-ink-900 dark:text-ink-100">
107
107
  {p.title}
108
108
  </h3>
109
- <p class="m-0 max-w-xs text-[0.875rem] leading-relaxed text-ink-500 dark:text-ink-400">
109
+ <p class="m-0 mt-1.5 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
110
110
  {p.body}
111
111
  </p>
112
112
  </li>
113
113
  ))}
114
- </ol>
114
+ </ul>
115
115
  </div>
116
116
  </section>
@@ -2,19 +2,18 @@
2
2
  /**
3
3
  * BaseLayout — wraps every page. Owns the theme-detection inline
4
4
  * script and the global shell (Header, Footer). Per-page SEO
5
- * (title, description, OG, JSON-LD) is delegated to <Seo /> so each
6
- * page can declare what it needs without polluting the layout.
5
+ * (title, description, OG, JSON-LD) is delegated to `<Seo />` so
6
+ * each page can declare what it needs without polluting the layout
7
+ * file.
7
8
  *
8
- * Generic: takes a `site` prop with brand + nav, no siteConfig import.
9
- * The header / footer / SEO components are imported from sibling
10
- * files; consumers can override the Header/Footer via Astro's slot
11
- * mechanism (`<Header slot="header" />`) if they need custom branding.
9
+ * Generic: takes a `site` prop with brand + nav + stats. No
10
+ * `siteConfig` import consumers compose the props from their
11
+ * own config (typically `data/generated/site-config.json` for the
12
+ * showcase).
12
13
  *
13
- * The THEME_INIT inline script persists user preference in
14
- * `localStorage` under the key "grove-theme" — matching the key
15
- * `ThemeToggle.astro` uses.
14
+ * Optional `gaId` adds a GA4 measurement snippet. Omit for
15
+ * privacy-first deployments.
16
16
  */
17
-
18
17
  import "../styles.css";
19
18
  import Header from "./Header.astro";
20
19
  import Footer from "./Footer.astro";
@@ -25,6 +24,18 @@ interface NavItem {
25
24
  href: string;
26
25
  }
27
26
 
27
+ interface SiteStats {
28
+ totalApps?: number;
29
+ totalCategories?: number;
30
+ totalStacks?: number;
31
+ totalOwners?: number;
32
+ totalStars?: number;
33
+ originalRepo?: string;
34
+ originalStars?: number;
35
+ originalForks?: number;
36
+ originalContributors?: number;
37
+ }
38
+
28
39
  interface Site {
29
40
  name: string;
30
41
  tagline?: string;
@@ -32,6 +43,15 @@ interface Site {
32
43
  nav: NavItem[];
33
44
  repoUrl?: string;
34
45
  url?: string;
46
+ /** Optional directory-level stats — populated from site-config.json
47
+ * at generate time. Components use it to render hero counts,
48
+ * origin card stats, and footer summary lines. */
49
+ stats?: SiteStats;
50
+ blueprintConfig?: {
51
+ routeSlug?: string;
52
+ labelSingular?: string;
53
+ labelPlural?: string;
54
+ };
35
55
  }
36
56
 
37
57
  interface Props {
@@ -43,7 +63,7 @@ interface Props {
43
63
  jsonLd?: Record<string, unknown> | Record<string, unknown>[];
44
64
  /** Optional Google Analytics 4 Measurement ID (e.g. "G-XXXXXX"). */
45
65
  gaId?: string;
46
- /** Path used for JSON-LD `SearchAction` target. Defaults to "/projects". */
66
+ /** Path used for JSON-LD `SearchAction` target. Defaults to "/items". */
47
67
  searchPath?: string;
48
68
  /** Submit button label rendered in the header. Omit to hide. */
49
69
  submitLabel?: string;
@@ -63,6 +83,8 @@ const {
63
83
  submitLabel,
64
84
  submitHref,
65
85
  } = Astro.props;
86
+ const directoryPath =
87
+ searchPath ?? `/${site.blueprintConfig?.routeSlug ?? "items"}`;
66
88
 
67
89
  const favicon =
68
90
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2318181b'/%3E%3C/svg%3E";
@@ -87,11 +109,6 @@ const favicon =
87
109
 
88
110
  {gaId && (
89
111
  <>
90
- {/* GA4 — gtag.js loader. We send the initial page_view with
91
- the current path + query string so the dashboard
92
- distinguishes /projects?stack=flutter from
93
- /projects?stack=react (otherwise they'd all collapse to
94
- /projects). */}
95
112
  <script
96
113
  is:inline
97
114
  async
@@ -118,7 +135,7 @@ const favicon =
118
135
  noindex={noindex}
119
136
  jsonLd={jsonLd}
120
137
  site={site}
121
- searchPath={searchPath}
138
+ searchPath={directoryPath}
122
139
  />
123
140
 
124
141
  <slot name="head" />
@@ -168,8 +185,8 @@ const favicon =
168
185
  <slot name="header">
169
186
  <Header
170
187
  site={site}
171
- submitLabel={submitLabel}
172
- submitHref={submitHref}
188
+ submitLabel={submitLabel ?? (site.blueprintConfig ? `Submit ${site.blueprintConfig.labelSingular ?? "item"}` : undefined)}
189
+ submitHref={submitHref ?? (site.blueprintConfig ? "/submit" : undefined)}
173
190
  />
174
191
  </slot>
175
192
 
@@ -178,17 +195,80 @@ const favicon =
178
195
  </main>
179
196
 
180
197
  <slot name="footer">
181
- <Footer site={site} />
198
+ <Footer
199
+ site={site}
200
+ stats={{
201
+ totalApps: site.stats?.totalApps,
202
+ totalCategories: site.stats?.totalCategories,
203
+ totalStacks: site.stats?.totalStacks,
204
+ totalOwners: site.stats?.totalOwners,
205
+ }}
206
+ itemsLabel={site.blueprintConfig?.labelPlural ?? "items"}
207
+ discover={{
208
+ heading: "Discover",
209
+ items: [
210
+ ...(site.stats?.totalApps
211
+ ? [{ label: "Browse all", href: directoryPath }]
212
+ : []),
213
+ { label: "Contributors", href: "/contributors" },
214
+ ...(site.stats?.originalRepo
215
+ ? [{ label: "View on GitHub", href: site.stats.originalRepo, external: true }]
216
+ : []),
217
+ ],
218
+ }}
219
+ contribute={{
220
+ heading: "Contribute",
221
+ items: [
222
+ ...(site.repoUrl
223
+ ? [
224
+ {
225
+ label: "Submit via GitHub",
226
+ href: `${site.repoUrl}/blob/main/CONTRIBUTING.md`,
227
+ external: true,
228
+ },
229
+ ]
230
+ : []),
231
+ ...(site.repoUrl
232
+ ? [
233
+ {
234
+ label: "Report an issue",
235
+ href: `${site.repoUrl}/issues`,
236
+ external: true,
237
+ },
238
+ ]
239
+ : []),
240
+ ],
241
+ }}
242
+ project={{
243
+ heading: "Project",
244
+ items: [
245
+ ...(site.repoUrl
246
+ ? [
247
+ {
248
+ label: "Source on GitHub",
249
+ href: site.repoUrl,
250
+ external: true,
251
+ },
252
+ ]
253
+ : []),
254
+ ...(site.repoUrl
255
+ ? [
256
+ {
257
+ label: "Changelog",
258
+ href: `${site.repoUrl}/blob/main/CHANGELOG.md`,
259
+ external: true,
260
+ },
261
+ ]
262
+ : []),
263
+ ],
264
+ }}
265
+ />
182
266
  </slot>
183
267
 
184
268
  <slot name="body-end" />
185
269
 
186
270
  {gaId && (
187
271
  <script is:inline define:vars={{ gaId }}>
188
- // Mirror in-page SPA navigation (history.pushState /
189
- // replaceState) into GA4 page_view events so filter changes
190
- // on /projects show up as separate page views in the
191
- // dashboard instead of one inflated session.
192
272
  (function () {
193
273
  var last = location.pathname + location.search;
194
274
  function report() {
@@ -215,30 +295,5 @@ const favicon =
215
295
  })();
216
296
  </script>
217
297
  )}
218
-
219
- {gaId && (
220
- <script is:inline>
221
- // Delegated click handler for `data-event` elements. We do
222
- // this in one place so individual components just emit data
223
- // attributes and stay declarative.
224
- (function () {
225
- document.addEventListener("click", function (e) {
226
- var el = e.target instanceof Element
227
- ? e.target.closest("[data-event]")
228
- : null;
229
- if (!el) return;
230
- var name = el.getAttribute("data-event");
231
- if (!name) return;
232
- var params = {
233
- source: el.getAttribute("data-event-source") || undefined,
234
- app_slug: el.getAttribute("data-event-app") || undefined,
235
- channel: el.getAttribute("data-event-channel") || undefined,
236
- link_url: el.getAttribute("href") || undefined,
237
- };
238
- window.gtag("event", name, params);
239
- });
240
- })();
241
- </script>
242
- )}
243
298
  </body>
244
299
  </html>
@@ -1,57 +1,18 @@
1
1
  ---
2
- /**
3
- * Container — page-width wrapper.
4
- *
5
- * Centralizes the three layout widths the showcase uses:
6
- *
7
- * - "default" → 1140px (the standard reading width)
8
- * - "wide" → 1400px (home, full-bleed grid sections)
9
- * - "narrow" → 720px (about, methodology, content-heavy pages)
10
- *
11
- * Pure layout, no styling dependencies beyond Tailwind's `max-w-*`
12
- * tokens defined in `@grove-dev/astro/styles.css`.
13
- */
14
-
15
2
  interface Props {
16
3
  size?: "default" | "wide" | "narrow";
17
4
  class?: string;
18
- /** Padding preset. Defaults to a comfortable 1.25rem / 1.75rem. */
19
- padding?: "default" | "none" | "tight";
20
5
  }
21
6
 
22
- const {
23
- size = "default",
24
- class: className = "",
25
- padding = "default",
26
- } = Astro.props;
27
-
7
+ const { size = "default", class: className = "" } = Astro.props;
28
8
  const sizeClass =
29
9
  size === "wide"
30
- ? "grove-container-wide"
10
+ ? "grove-container-wide max-w-wide"
31
11
  : size === "narrow"
32
- ? "grove-container-narrow"
33
- : "grove-container";
34
-
35
- const paddingClass =
36
- padding === "none"
37
- ? ""
38
- : padding === "tight"
39
- ? "px-4 sm:px-5"
40
- : "px-5 sm:px-7";
12
+ ? "grove-container-narrow max-w-narrow"
13
+ : "grove-container max-w-container";
41
14
  ---
42
15
 
43
- <div class:list={["mx-auto", sizeClass, paddingClass, className]}>
16
+ <div class={`${sizeClass} mx-auto px-5 sm:px-7 ${className}`}>
44
17
  <slot />
45
18
  </div>
46
-
47
- <style>
48
- .grove-container {
49
- max-width: var(--container-container, 1140px);
50
- }
51
- .grove-container-wide {
52
- max-width: var(--container-wide, 1400px);
53
- }
54
- .grove-container-narrow {
55
- max-width: var(--container-narrow, 720px);
56
- }
57
- </style>
@@ -3,13 +3,14 @@
3
3
  * Footer — 4-column grid (brand + Discover + Contribute + Project) plus
4
4
  * a thin bar at the bottom with copyright + repo link.
5
5
  *
6
- * Generic: takes a `site` prop with brand + repoUrl. No siteConfig
7
- * import consumers compose props from their own config.
6
+ * Generic: takes a `site` prop with brand + repoUrl + stats, plus
7
+ * pre-computed column data. No `siteConfig` import consumers
8
+ * compose props from their own config (or from
9
+ * `data/generated/site-config.json` for the showcase).
8
10
  *
9
11
  * Each column's links are passed in as a `{ heading, items[] }` shape
10
- * so consumers can customize the order and copy. Defaults are provided
11
- * for the brand and bar so the footer still renders if no extra
12
- * link data is supplied.
12
+ * so consumers can customize the order and copy. Defaults are
13
+ * provided so the footer still renders if no extra data is supplied.
13
14
  */
14
15
 
15
16
  interface NavItem {
@@ -29,6 +30,12 @@ interface Site {
29
30
  tagline?: string;
30
31
  description?: string;
31
32
  repoUrl?: string;
33
+ stats?: {
34
+ totalApps?: number;
35
+ totalCategories?: number;
36
+ totalStacks?: number;
37
+ totalOwners?: number;
38
+ };
32
39
  }
33
40
 
34
41
  interface Props {
@@ -39,8 +46,13 @@ interface Props {
39
46
  contribute?: FooterColumn;
40
47
  /** Project links — third content column. */
41
48
  project?: FooterColumn;
42
- /** Stats line under brand blurb (e.g. "79 apps · 12 categories"). */
43
- stats?: { apps?: number; categories?: number; contributors?: number };
49
+ /** Stats line under brand blurb. */
50
+ stats?: { totalApps?: number; totalCategories?: number; totalOwners?: number };
51
+ /**
52
+ * Plural label for the primary count stat. Default "items" so the
53
+ * footer reads naturally regardless of blueprint.
54
+ */
55
+ itemsLabel?: string;
44
56
  /** Copyright suffix. Defaults to site.name. */
45
57
  copyrightHolder?: string;
46
58
  /** License line under the bar (e.g. "Content under CC BY-SA 4.0."). */
@@ -55,6 +67,7 @@ const {
55
67
  contribute = { heading: "Contribute", items: [] },
56
68
  project = { heading: "Project", items: [] },
57
69
  stats,
70
+ itemsLabel = "items",
58
71
  copyrightHolder,
59
72
  licenseLine,
60
73
  class: className = "",
@@ -96,8 +109,7 @@ const projectItems = project.items.length ? project.items : defaultProject;
96
109
  ]}
97
110
  >
98
111
  <div
99
- class="mx-auto grid max-w-wide grid-cols-2 gap-8 px-5 py-12 md:grid-cols-4 sm:px-7"
100
- style="max-width: var(--container-wide, 1400px);"
112
+ class="grove-container-wide mx-auto grid max-w-wide grid-cols-2 gap-8 px-5 py-12 md:grid-cols-4 sm:px-7"
101
113
  >
102
114
  <div class="col-span-2 md:col-span-1">
103
115
  <div
@@ -116,25 +128,25 @@ const projectItems = project.items.length ? project.items : defaultProject;
116
128
  )}
117
129
  {stats && (
118
130
  <p class="mt-3 m-0 text-2xs text-ink-500 dark:text-ink-400">
119
- {stats.apps !== undefined && (
131
+ {stats.totalApps !== undefined && (
120
132
  <>
121
- <span class="font-mono">{stats.apps}</span> apps
133
+ <span class="font-mono">{stats.totalApps}</span> {itemsLabel}
122
134
  </>
123
135
  )}
124
- {stats.apps !== undefined && (stats.categories !== undefined || stats.contributors !== undefined) && (
136
+ {stats.totalApps !== undefined && (stats.totalCategories !== undefined || stats.totalOwners !== undefined) && (
125
137
  <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
126
138
  )}
127
- {stats.categories !== undefined && (
139
+ {stats.totalCategories !== undefined && (
128
140
  <>
129
- <span class="font-mono">{stats.categories}</span> categories
141
+ <span class="font-mono">{stats.totalCategories}</span> categories
130
142
  </>
131
143
  )}
132
- {stats.categories !== undefined && stats.contributors !== undefined && (
144
+ {stats.totalCategories !== undefined && stats.totalOwners !== undefined && (
133
145
  <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
134
146
  )}
135
- {stats.contributors !== undefined && (
147
+ {stats.totalOwners !== undefined && (
136
148
  <>
137
- <span class="font-mono">{stats.contributors}</span> contributors
149
+ <span class="font-mono">{stats.totalOwners}</span> contributors
138
150
  </>
139
151
  )}
140
152
  </p>
@@ -213,10 +225,9 @@ const projectItems = project.items.length ? project.items : defaultProject;
213
225
 
214
226
  <div class="border-t border-ink-200 dark:border-ink-800">
215
227
  <div
216
- class="mx-auto flex max-w-wide flex-wrap items-center gap-3 px-5 py-4 text-2xs text-ink-500 sm:px-7 dark:text-ink-400"
217
- style="max-width: var(--container-wide, 1400px);"
228
+ class="grove-container-wide mx-auto flex max-w-wide flex-wrap items-center gap-3 px-5 py-4 text-2xs text-ink-500 sm:px-7 dark:text-ink-400"
218
229
  >
219
- <span>© {year} {holder}.{licenseLine ? ` ${licenseLine}` : ""}</span>
230
+ <span>© {year} {holder}{licenseLine ? `. ${licenseLine}` : "."}</span>
220
231
  {gh && (
221
232
  <>
222
233
  <span class="text-ink-300 dark:text-ink-700">·</span>
@@ -26,6 +26,13 @@ interface Site {
26
26
  nav: NavItem[];
27
27
  description?: string;
28
28
  repoUrl?: string;
29
+ url?: string;
30
+ stats?: {
31
+ totalStars?: number;
32
+ };
33
+ blueprintConfig?: {
34
+ routeSlug?: string;
35
+ };
29
36
  }
30
37
 
31
38
  interface Props {
@@ -47,6 +54,21 @@ const {
47
54
  repoLabel = "Star",
48
55
  class: className = "",
49
56
  } = Astro.props;
57
+ const directoryPath = `/${site.blueprintConfig?.routeSlug ?? "items"}`;
58
+ const navItems = site.blueprintConfig
59
+ ? [
60
+ { label: "Recently added", href: `${directoryPath}?label=new` },
61
+ { label: "Trending", href: `${directoryPath}?label=hot` },
62
+ { label: "Categories", href: directoryPath },
63
+ { label: "Stacks", href: directoryPath },
64
+ { label: "About", href: "/about" },
65
+ ]
66
+ : site.nav;
67
+ const stars = site.stats?.totalStars ?? 0;
68
+ const starsLabel =
69
+ stars >= 1000
70
+ ? `${(stars / 1000).toFixed(stars >= 10000 ? 0 : 1)}k`
71
+ : String(stars);
50
72
  ---
51
73
 
52
74
  <header
@@ -56,8 +78,7 @@ const {
56
78
  ]}
57
79
  >
58
80
  <div
59
- class="mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
60
- style="max-width: var(--container-wide, 1400px);"
81
+ class="grove-container-wide mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
61
82
  >
62
83
  <a
63
84
  href="/"
@@ -72,7 +93,7 @@ const {
72
93
 
73
94
  <nav class="hidden flex-1 md:block" aria-label="Primary">
74
95
  <ul class="m-0 flex list-none gap-5 p-0">
75
- {site.nav.map((item) => (
96
+ {navItems.map((item) => (
76
97
  <li>
77
98
  <a
78
99
  href={item.href}
@@ -89,7 +110,7 @@ const {
89
110
  {submitHref && (
90
111
  <a
91
112
  href={submitHref}
92
- class="grove-btn grove-btn-outline"
113
+ class="hidden h-9 items-center gap-1.5 rounded-md border border-ink-200 bg-white px-3 text-[0.875rem] font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 sm:inline-flex dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
93
114
  >
94
115
  <svg
95
116
  viewBox="0 0 16 16"
@@ -109,22 +130,22 @@ const {
109
130
  href={site.repoUrl}
110
131
  target="_blank"
111
132
  rel="noopener noreferrer"
112
- aria-label={repoLabel}
113
- title={repoLabel}
114
- class="grove-btn grove-btn-outline"
133
+ aria-label={`${repoLabel} on GitHub — ${stars} stars`}
134
+ title={`${repoLabel} on GitHub`}
135
+ class="inline-flex h-9 items-center divide-x divide-ink-200 overflow-hidden rounded-md border border-ink-200 text-[0.875rem] font-medium transition-colors hover:border-ink-300 hover:bg-ink-50 dark:divide-ink-800 dark:border-ink-800 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
115
136
  >
116
- <svg
117
- viewBox="0 0 16 16"
118
- width="16"
119
- height="16"
120
- aria-hidden="true"
121
- fill="currentColor"
122
- >
123
- <path
124
- d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"
125
- ></path>
126
- </svg>
127
- <span>{repoLabel}</span>
137
+ <span class="flex items-center gap-1.5 px-2.5 py-1.5 text-ink-900 dark:text-ink-100">
138
+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="currentColor">
139
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"></path>
140
+ </svg>
141
+ <span>{repoLabel}</span>
142
+ </span>
143
+ <span class="flex items-center gap-1 bg-ink-50 px-2 py-1.5 text-ink-700 dark:bg-ink-900/60 dark:text-ink-300">
144
+ <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
145
+ <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path>
146
+ </svg>
147
+ <span class="font-mono">{starsLabel}</span>
148
+ </span>
128
149
  </a>
129
150
  )}
130
151