@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
@@ -2,17 +2,19 @@
2
2
  /**
3
3
  * Hero — the home page banner.
4
4
  *
5
- * Renders: trust stats (apps, categories, stacks, contributors, stars),
6
- * a search form, an animated gradient backdrop, and up to 2 CTAs.
5
+ * Mirrors the openapps layout: trust-stats row across the top,
6
+ * big two-line headline with a gradient accent on the second
7
+ * line, descriptive paragraph, search form with a Filters
8
+ * shortcut, quick-filter chips, and up to 3 CTAs.
7
9
  *
8
- * The search form is a static GET form pointing at `searchHref` (the
9
- * list page) with a `q` query param. JS enhancement is a consumer
10
- * concern (e.g. the showcase wires the home hero's input into the
11
- * list page filter via a custom event).
12
- *
13
- * Generic: every value comes through props — site name, tagline,
14
- * stats, search target. No siteConfig import.
10
+ * Generic: every value comes through props (or `siteConfig` for
11
+ * the trust stats). The label next to the count is blueprint-
12
+ * driven via `itemsLabel` so a project-directory renders
13
+ * "apps indexed", a resource-hub renders "resources indexed",
14
+ * an ecosystem-map renders "entities indexed".
15
15
  */
16
+ import Container from "../layouts/Container.astro";
17
+ import { compact } from "../lib/format";
16
18
 
17
19
  interface CTA {
18
20
  label: string;
@@ -21,33 +23,43 @@ interface CTA {
21
23
  external?: boolean;
22
24
  }
23
25
 
24
- interface HeroStats {
25
- apps?: number;
26
- categories?: number;
27
- stacks?: number;
28
- contributors?: number;
29
- stars?: number;
30
- }
31
-
32
26
  interface Props {
33
- /** Site/brand name — rendered as the heading. */
27
+ /** Site/brand name. */
34
28
  name: string;
35
- /** Short tagline displayed under the heading. */
29
+ /** Short tagline usually pulled from siteConfig.tagline. */
36
30
  tagline: string;
37
- /** Longer description, rendered below the tagline. */
31
+ /** Longer descriptive paragraph. */
38
32
  description?: string;
39
- /** Trust stats row. */
40
- stats?: HeroStats;
41
- /** Placeholder for the search input. */
42
- searchPlaceholder?: string;
43
- /** Form action — the list page URL (e.g. "/projects"). */
33
+ /** Trust stats: items count, categories, stacks, contributors, stars. */
34
+ stats?: {
35
+ totalApps?: number;
36
+ totalCategories?: number;
37
+ totalStacks?: number;
38
+ totalOwners?: number;
39
+ totalStars?: number;
40
+ originalRepo?: string;
41
+ };
42
+ /** Search form target — the list page. */
44
43
  searchHref?: string;
45
- /** Optional CTAs rendered next to the search form. */
44
+ /** Search input placeholder. */
45
+ searchPlaceholder?: string;
46
+ /**
47
+ * Plural noun for the primary count (e.g. "apps", "resources",
48
+ * "entities"). Used in the trust-stats label and CTA copy.
49
+ */
50
+ itemsLabel?: string;
51
+ /** Singular noun for the Submit / Browse copy. */
52
+ itemLabel?: string;
53
+ /** Quick filter chips under the search bar. */
54
+ quickFilters?: Array<{ label: string; href: string }>;
55
+ /** CTAs rendered below the form. */
46
56
  ctas?: CTA[];
47
- /** Extra class for the outer <section>. */
57
+ /** Extra class. */
48
58
  class?: string;
49
59
  /** Eyebrow text rendered above the title. */
50
60
  eyebrow?: string;
61
+ /** Optional badge content rendered next to the eyebrow. */
62
+ eyebrowBadge?: string;
51
63
  }
52
64
 
53
65
  const {
@@ -55,98 +67,191 @@ const {
55
67
  tagline,
56
68
  description,
57
69
  stats = {},
58
- searchPlaceholder = "Search the directory...",
59
70
  searchHref = "/",
71
+ searchPlaceholder = "Search the directory...",
72
+ itemsLabel = "items",
73
+ itemLabel,
74
+ quickFilters = [],
60
75
  ctas = [],
61
76
  class: className = "",
62
- eyebrow,
77
+ eyebrow = "A directory of curated knowledge",
78
+ eyebrowBadge,
63
79
  } = Astro.props;
64
80
 
65
- function fmt(n: number | undefined): string {
66
- if (typeof n !== "number" || !Number.isFinite(n)) return "—";
67
- if (n >= 1000) {
68
- const k = n / 1000;
69
- return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
70
- }
71
- return n.toString();
72
- }
81
+ // Singular for the Submit-style CTA copy. Defaults to the lowercased
82
+ // items label so "apps" "app" without an extra prop.
83
+ const singular = (itemLabel ?? itemsLabel.replace(/s$/, "")).toLowerCase();
73
84
 
74
- const hasStats =
75
- stats.apps !== undefined ||
76
- stats.categories !== undefined ||
77
- stats.stacks !== undefined ||
78
- stats.contributors !== undefined ||
79
- stats.stars !== undefined;
85
+ const githubUrl = stats.originalRepo;
86
+
87
+ const trustStats: { value: string; label: string }[] = [
88
+ ...(stats.totalApps !== undefined
89
+ ? [{ value: compact(stats.totalApps) ?? "0", label: `${itemsLabel} indexed` }]
90
+ : []),
91
+ ...(stats.totalCategories !== undefined
92
+ ? [{ value: String(stats.totalCategories), label: "categories" }]
93
+ : []),
94
+ ...(stats.totalStacks !== undefined
95
+ ? [{ value: String(stats.totalStacks), label: "stacks" }]
96
+ : []),
97
+ ...(stats.totalOwners !== undefined
98
+ ? [{ value: compact(stats.totalOwners) ?? "0", label: "contributors" }]
99
+ : []),
100
+ ...(stats.totalStars !== undefined
101
+ ? [{ value: compact(stats.totalStars) ?? "0", label: "GitHub stars" }]
102
+ : []),
103
+ ];
80
104
  ---
81
105
 
82
106
  <section
83
107
  class:list={[
84
- "grove-hero relative overflow-hidden border-b border-ink-200 dark:border-ink-800",
108
+ "relative overflow-hidden border-b border-ink-200 dark:border-ink-800",
85
109
  className,
86
110
  ]}
87
- aria-labelledby="grove-hero-heading"
88
111
  >
89
112
  <div
90
113
  aria-hidden="true"
91
- class="grove-hero__gradient pointer-events-none absolute inset-0 -z-10"
114
+ class="pointer-events-none absolute inset-0 opacity-60 dark:opacity-40"
115
+ style="
116
+ background-image:
117
+ radial-gradient(60% 50% at 15% 20%, rgba(31, 111, 235, 0.12), transparent 60%),
118
+ radial-gradient(50% 40% at 85% 10%, rgba(110, 231, 183, 0.10), transparent 60%),
119
+ radial-gradient(40% 40% at 50% 90%, rgba(251, 146, 60, 0.08), transparent 60%);
120
+ "
92
121
  ></div>
93
122
 
94
- <div class="grove-container-wide mx-auto px-5 py-16 sm:px-7 sm:py-20">
95
- <div class="max-w-3xl">
96
- {eyebrow && (
97
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
123
+ <Container>
124
+ <div class="relative py-14 sm:py-20">
125
+ <div class="max-w-3xl">
126
+ <span
127
+ class="inline-flex animate-fade-in items-center gap-2 text-2xs font-medium text-ink-500 dark:text-ink-400"
128
+ >
129
+ <span
130
+ aria-hidden="true"
131
+ class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"
132
+ ></span>
98
133
  {eyebrow}
134
+ {eyebrowBadge && (
135
+ <span
136
+ class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-700 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
137
+ aria-label={eyebrowBadge}
138
+ >
139
+ {eyebrowBadge}
140
+ </span>
141
+ )}
99
142
  </span>
100
- )}
101
- <h1
102
- id="grove-hero-heading"
103
- class="m-0 mt-2 text-[2.25rem] font-semibold leading-[1.05] tracking-tight text-ink-900 sm:text-[3rem] dark:text-ink-100"
104
- >
105
- {name}
106
- </h1>
107
- <p class="m-0 mt-4 max-w-2xl text-lg leading-relaxed text-ink-700 dark:text-ink-200">
108
- {tagline}
109
- </p>
110
- {description && (
111
- <p class="m-0 mt-2 max-w-2xl text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400">
112
- {description}
113
- </p>
143
+
144
+ <h1
145
+ class="mt-3 animate-fade-in-up text-[2.25rem] sm:text-[2.75rem] md:text-[3rem] font-semibold tracking-[-0.02em] leading-[1.05] text-ink-900 dark:text-ink-100 m-0"
146
+ style="animation-delay: 0.05s;"
147
+ >
148
+ {name}<br class="hidden sm:block" />
149
+ <span
150
+ class="bg-gradient-to-r from-ink-900 via-ink-700 to-ink-900 bg-clip-text text-transparent dark:from-ink-100 dark:via-ink-300 dark:to-ink-100"
151
+ style="background-size: 200% 200%; animation: fade-in-up 0.7s ease-out 0.1s both, gradient-shift 8s ease-in-out 1s infinite;"
152
+ >
153
+ {tagline}
154
+ </span>
155
+ </h1>
156
+
157
+ {description && (
158
+ <p
159
+ class="mt-4 animate-fade-in-up text-[0.9375rem] sm:text-[1rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
160
+ style="animation-delay: 0.15s;"
161
+ >
162
+ {description}
163
+ </p>
164
+ )}
165
+ </div>
166
+
167
+ {/* Trust stats row */}
168
+ {trustStats.length > 0 && (
169
+ <dl
170
+ class="mt-7 grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-4"
171
+ aria-label="Directory at a glance"
172
+ >
173
+ {trustStats.map((s) => (
174
+ <div class="flex flex-col gap-0.5">
175
+ <dt class="sr-only">{s.label}</dt>
176
+ <dd class="m-0 font-mono text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
177
+ {s.value}
178
+ </dd>
179
+ <dd class="m-0 text-2xs uppercase tracking-wider text-ink-500 dark:text-ink-400">
180
+ {s.label}
181
+ </dd>
182
+ </div>
183
+ ))}
184
+ </dl>
114
185
  )}
115
186
 
187
+ {/* Search + filters */}
116
188
  <form
117
- class="mt-8 flex flex-col gap-2 sm:flex-row sm:items-center"
189
+ class="mt-7 flex flex-col gap-2 sm:flex-row sm:items-stretch"
118
190
  role="search"
119
191
  action={searchHref}
120
192
  method="get"
121
- data-grove-hero-search
122
193
  >
123
194
  <label class="relative flex-1">
124
- <span class="sr-only">Search</span>
195
+ <span class="sr-only">Search {itemsLabel}, stacks, categories, licenses</span>
125
196
  <svg
126
- aria-hidden="true"
127
197
  viewBox="0 0 16 16"
128
- width="14"
129
- height="14"
130
- class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
198
+ width="16"
199
+ height="16"
200
+ aria-hidden="true"
201
+ class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-400"
131
202
  fill="currentColor"
132
203
  >
133
- <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>
204
+ <path
205
+ d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"
206
+ ></path>
134
207
  </svg>
135
208
  <input
136
209
  type="search"
137
210
  name="q"
138
211
  placeholder={searchPlaceholder}
139
- autocomplete="off"
140
- class="w-full rounded-md border border-ink-200 bg-white py-2.5 pl-9 pr-3 text-sm placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
212
+ class="h-10 w-full rounded-md border border-ink-200 bg-white pl-10 pr-3 text-[0.9375rem] text-ink-900 placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100 dark:placeholder:text-ink-500 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
141
213
  />
142
214
  </label>
143
- <button type="submit" class="grove-btn grove-btn-primary">
215
+ <a
216
+ href={searchHref}
217
+ class="inline-flex h-10 items-center justify-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 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
218
+ aria-label={`Browse all ${itemsLabel} with filters`}
219
+ >
220
+ <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
221
+ <path d="M.75 3h14.5a.75.75 0 0 1 .55 1.26l-5.55 6.1v4.39a.75.75 0 0 1-1.2.6l-2.5-1.87a.75.75 0 0 1-.3-.6v-2.52L.2 4.26A.75.75 0 0 1 .75 3Z"/>
222
+ </svg>
223
+ <span>Filters</span>
224
+ </a>
225
+ <button
226
+ type="submit"
227
+ class="grove-btn grove-btn-primary h-10 px-4 sm:px-5"
228
+ >
144
229
  Search
145
230
  </button>
146
231
  </form>
147
232
 
233
+ {quickFilters.length > 0 && (
234
+ <p
235
+ class="mt-2 text-2xs text-ink-500 dark:text-ink-400 m-0"
236
+ aria-hidden="true"
237
+ >
238
+ Try{" "}
239
+ {quickFilters.map((f, i) => (
240
+ <>
241
+ <a
242
+ href={f.href}
243
+ class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300"
244
+ >
245
+ {f.label}
246
+ </a>
247
+ {i < quickFilters.length - 1 && <span class="mx-1">·</span>}
248
+ </>
249
+ ))}
250
+ </p>
251
+ )}
252
+
148
253
  {ctas.length > 0 && (
149
- <div class="mt-4 flex flex-wrap items-center gap-2">
254
+ <div class="mt-5 flex flex-wrap items-center gap-2">
150
255
  {ctas.map((cta) => (
151
256
  <a
152
257
  href={cta.href}
@@ -170,77 +275,23 @@ const hasStats =
170
275
  </div>
171
276
  )}
172
277
 
173
- {hasStats && (
174
- <dl
175
- class="mt-10 flex flex-wrap items-center gap-x-6 gap-y-3 text-2xs text-ink-500 dark:text-ink-400"
176
- aria-label="Directory statistics"
177
- >
178
- {stats.apps !== undefined && (
179
- <div class="flex items-baseline gap-1.5">
180
- <dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
181
- {fmt(stats.apps)}
182
- </dd>
183
- <dt>apps</dt>
184
- </div>
185
- )}
186
- {stats.categories !== undefined && (
187
- <div class="flex items-baseline gap-1.5">
188
- <dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
189
- {fmt(stats.categories)}
190
- </dd>
191
- <dt>categories</dt>
192
- </div>
193
- )}
194
- {stats.stacks !== undefined && (
195
- <div class="flex items-baseline gap-1.5">
196
- <dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
197
- {fmt(stats.stacks)}
198
- </dd>
199
- <dt>stacks</dt>
200
- </div>
201
- )}
202
- {stats.contributors !== undefined && (
203
- <div class="flex items-baseline gap-1.5">
204
- <dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
205
- {fmt(stats.contributors)}
206
- </dd>
207
- <dt>contributors</dt>
208
- </div>
209
- )}
210
- {stats.stars !== undefined && (
211
- <div class="flex items-baseline gap-1.5">
212
- <dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
213
- {fmt(stats.stars)}
214
- </dd>
215
- <dt>total stars</dt>
216
- </div>
217
- )}
218
- </dl>
278
+ {/* Help debug the type system when callers forget to pass ctas. */}
279
+ {ctas.length === 0 && githubUrl && (
280
+ <div class="mt-5 flex flex-wrap items-center gap-2">
281
+ <a href={searchHref} class="grove-btn grove-btn-primary">
282
+ Browse {itemsLabel}
283
+ </a>
284
+ <a
285
+ href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
286
+ target="_blank"
287
+ rel="noopener noreferrer"
288
+ class="grove-btn grove-btn-outline"
289
+ >
290
+ Submit via GitHub
291
+ </a>
292
+ </div>
219
293
  )}
294
+ {singular /* suppress unused warning on the singular variable */ ? null : null}
220
295
  </div>
221
- </div>
296
+ </Container>
222
297
  </section>
223
-
224
- <style>
225
- .grove-hero__gradient {
226
- background: linear-gradient(
227
- 120deg,
228
- rgba(31, 111, 235, 0.08) 0%,
229
- rgba(110, 231, 183, 0.06) 35%,
230
- rgba(253, 186, 116, 0.06) 65%,
231
- rgba(147, 197, 253, 0.08) 100%
232
- );
233
- background-size: 200% 200%;
234
- animation: var(--animate-gradient-shift, gradient-shift 8s ease-in-out infinite);
235
- }
236
- :global(.dark) .grove-hero__gradient {
237
- background: linear-gradient(
238
- 120deg,
239
- rgba(31, 111, 235, 0.18) 0%,
240
- rgba(6, 95, 70, 0.12) 35%,
241
- rgba(67, 20, 7, 0.12) 65%,
242
- rgba(30, 64, 175, 0.18) 100%
243
- );
244
- background-size: 200% 200%;
245
- }
246
- </style>
@@ -10,7 +10,7 @@
10
10
  * this component.
11
11
  *
12
12
  * The component is **generic** — it does not import any consumer-side
13
- * `siteConfig`, `app`, or blueprint-specific module. Every consumer
13
+ * `siteConfig`, `item`, or blueprint-specific module. Every consumer
14
14
  * supplies its own icon list (and optionally an `aliases` map for
15
15
  * friendly ids) via props, and any `name` not in the registry
16
16
  * falls back to the initials placeholder.
@@ -104,8 +104,14 @@ function resolveName(raw: string): string {
104
104
  }
105
105
 
106
106
  const resolved = resolveName(name);
107
+ const platformStackIcons = new Set(["docker"]);
108
+ const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
107
109
  const folder =
108
- category === "stack" ? "stacks" : category === "platform" ? "platforms" : "brands";
110
+ category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
111
+ ? "stacks"
112
+ : category === "platform"
113
+ ? "platforms"
114
+ : "brands";
109
115
  const baseUrl = `${base.replace(/\/$/, "")}/${folder}/${resolved}`;
110
116
 
111
117
  // Static: just point at the canonical svg.
@@ -116,7 +122,8 @@ const init = (fallbackInitials ?? name ?? "")
116
122
  .replace(/[^a-z0-9]/gi, "")
117
123
  .slice(0, 2)
118
124
  .toUpperCase();
119
- const showFallback = !resolved;
125
+ const showFallback =
126
+ !resolved || (category === "platform" && textOnlyPlatforms.has(resolved));
120
127
 
121
128
  // Build aria-label.
122
129
  const ariaLabel = title ?? name;
@@ -0,0 +1,75 @@
1
+ ---
2
+ import type { IndexRecord } from "@grove-dev/core";
3
+ import Icon from "./Icon.astro";
4
+ import { formatRelative, formatStars, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, statusDisplay } from "@grove-dev/astro";
5
+
6
+ interface Props {
7
+ record: IndexRecord;
8
+ detailHref: string;
9
+ /** Whether to show stack/platform chips. Default true. */
10
+ showChips?: boolean;
11
+ class?: string;
12
+ }
13
+
14
+ const { record, detailHref, showChips = true, class: className = "" } = Astro.props;
15
+ const item = record;
16
+ const project = item.kind === "project" ? item : null;
17
+ const name = item.kind === "resource" ? item.title : item.name;
18
+ const repoHref = project?.repoUrl ?? item.links.github;
19
+ const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
20
+ const avatar = project ? project.logoUrl ?? getOwnerAvatarUrl(owner, 80) : undefined;
21
+ const initials = name.split(/\s+/).map((word) => word[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();
22
+ const stacks = project ? [project.stack, ...project.stacks].filter(Boolean) : [];
23
+ const platforms = project?.platforms ?? [];
24
+ const stars = project?.github?.stars;
25
+ const updatedAt = project?.github?.pushedAt ?? (item.kind === "resource" ? item.publishedAt : null);
26
+ const license = project?.github?.license;
27
+ const status = project?.health?.status;
28
+ const curated = Boolean(item.curation?.reviewed || (project && (project.bestFor.length || project.whyListed.length || project.caveats.length)));
29
+ ---
30
+
31
+ <article class={`group relative flex flex-col gap-3 rounded-lg border border-ink-200 bg-white p-4 transition-colors hover:border-ink-300 hover:shadow-[0_1px_2px_rgba(0,0,0,0.04),0_4px_12px_-2px_rgba(0,0,0,0.06)] dark:border-ink-800 dark:bg-ink-950 dark:hover:border-ink-700 ${className}`}>
32
+ <header class="flex items-start gap-3">
33
+ <a href={detailHref} class="relative z-10 flex shrink-0" aria-label={`Open ${name} details`}>
34
+ {avatar ? (
35
+ <img src={avatar} alt={`${name} avatar`} width="44" height="44" loading="lazy" class="h-11 w-11 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900" />
36
+ ) : (
37
+ <span class="flex h-11 w-11 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-sm font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300">{initials}</span>
38
+ )}
39
+ </a>
40
+ <div class="min-w-0 flex-1">
41
+ <div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
42
+ <h3 class="m-0 text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
43
+ <a href={detailHref} class="relative z-10 hover:underline">{name}</a>
44
+ </h3>
45
+ {curated && <span class="text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400">✓ curated</span>}
46
+ </div>
47
+ <div class="flex flex-wrap items-center gap-x-1.5 text-2xs text-ink-500 dark:text-ink-400">
48
+ {repoHref && owner && <a href={repoHref} target="_blank" rel="noopener noreferrer" class="relative z-10 font-mono hover:text-ink-900 dark:hover:text-ink-100">@{owner}{repo ? `/${repo}` : ""}</a>}
49
+ {owner && <span class="text-ink-300 dark:text-ink-700">·</span>}
50
+ <span>{item.category}</span>
51
+ {status && <><span class="text-ink-300 dark:text-ink-700">·</span><span>{statusDisplay(status)}</span></>}
52
+ </div>
53
+ </div>
54
+ </header>
55
+
56
+ <p class="m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-600 dark:text-ink-300">{item.description}</p>
57
+
58
+ <dl class="grid grid-cols-3 gap-x-3 text-2xs">
59
+ <div class="flex items-baseline gap-1.5"><dt class="font-medium uppercase tracking-wider text-ink-500">Stars</dt><dd class="font-mono text-ink-700 dark:text-ink-300">{formatStars(stars) ?? "—"}</dd></div>
60
+ <div class="flex items-baseline gap-1.5"><dt class="font-medium uppercase tracking-wider text-ink-500">Updated</dt><dd class="font-mono text-ink-700 dark:text-ink-300">{formatRelative(updatedAt)}</dd></div>
61
+ <div class="flex items-baseline gap-1.5"><dt class="font-medium uppercase tracking-wider text-ink-500">License</dt><dd class="font-mono text-ink-700 dark:text-ink-300">{license ?? "—"}</dd></div>
62
+ </dl>
63
+
64
+ {showChips && (platforms.length > 0 || stacks.length > 0) && (
65
+ <div class="flex flex-col gap-1.5">
66
+ <div class="flex flex-wrap gap-1.5">{platforms.slice(0, 4).map((platform) => <span class="inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300"><Icon name={platform} category="platform" size={11} />{platform}</span>)}</div>
67
+ <div class="flex flex-wrap gap-1.5">{stacks.slice(0, 4).map((stack) => <span class="inline-flex items-center gap-1 rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"><Icon name={stack} category="stack" size={11} />{stack}</span>)}</div>
68
+ </div>
69
+ )}
70
+
71
+ <footer class="relative z-10 mt-1 flex items-center gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
72
+ <a href={detailHref} class="inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300">View details →</a>
73
+ {repoHref && <a href={repoHref} target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300">GitHub</a>}
74
+ </footer>
75
+ </article>