@grove-dev/astro 0.1.5 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +56 -22
  4. package/dist/index.js.map +1 -1
  5. package/dist/layouts/index.d.ts +25 -0
  6. package/dist/layouts/index.d.ts.map +1 -0
  7. package/dist/layouts/index.js +25 -0
  8. package/dist/layouts/index.js.map +1 -0
  9. package/dist/lib/display.d.ts +33 -0
  10. package/dist/lib/display.d.ts.map +1 -0
  11. package/dist/lib/display.js +104 -0
  12. package/dist/lib/display.js.map +1 -0
  13. package/dist/lib/format.d.ts +30 -0
  14. package/dist/lib/format.d.ts.map +1 -0
  15. package/dist/lib/format.js +73 -0
  16. package/dist/lib/format.js.map +1 -0
  17. package/dist/lib/index.d.ts +26 -0
  18. package/dist/lib/index.d.ts.map +1 -0
  19. package/dist/lib/index.js +26 -0
  20. package/dist/lib/index.js.map +1 -0
  21. package/dist/lib/lenses.d.ts +96 -0
  22. package/dist/lib/lenses.d.ts.map +1 -0
  23. package/dist/lib/lenses.js +181 -0
  24. package/dist/lib/lenses.js.map +1 -0
  25. package/dist/lib/repo.d.ts +30 -0
  26. package/dist/lib/repo.d.ts.map +1 -0
  27. package/dist/lib/repo.js +44 -0
  28. package/dist/lib/repo.js.map +1 -0
  29. package/dist/lib/scores.d.ts +38 -0
  30. package/dist/lib/scores.d.ts.map +1 -0
  31. package/dist/lib/scores.js +83 -0
  32. package/dist/lib/scores.js.map +1 -0
  33. package/dist/lib/search.d.ts +199 -0
  34. package/dist/lib/search.d.ts.map +1 -0
  35. package/dist/lib/search.js +357 -0
  36. package/dist/lib/search.js.map +1 -0
  37. package/dist/lib/taxonomy-counts.d.ts +25 -0
  38. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  39. package/dist/lib/taxonomy-counts.js +41 -0
  40. package/dist/lib/taxonomy-counts.js.map +1 -0
  41. package/package.json +7 -7
  42. package/src/components/AppSection.astro +83 -0
  43. package/src/components/AppsIndexRow.astro +204 -0
  44. package/src/components/AppsPagination.astro +126 -0
  45. package/src/components/CategoryGrid.astro +86 -17
  46. package/src/components/ContributorsGrid.astro +149 -0
  47. package/src/components/DecisionRow.astro +89 -0
  48. package/src/components/ExploreByCategory.astro +67 -0
  49. package/src/components/ExploreByStack.astro +72 -0
  50. package/src/components/Hero.astro +246 -0
  51. package/src/components/Icon.astro +223 -0
  52. package/src/components/ItemCard.astro +289 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RefinePanel.astro +197 -0
  56. package/src/components/ScoreBars.astro +92 -22
  57. package/src/components/SmartLensTabs.astro +71 -0
  58. package/src/components/StackGrid.astro +132 -0
  59. package/src/components/WhyThisExists.astro +116 -0
  60. package/src/index.ts +61 -14
  61. package/src/layouts/BaseLayout.astro +227 -15
  62. package/src/layouts/Container.astro +57 -0
  63. package/src/layouts/Footer.astro +235 -0
  64. package/src/layouts/Header.astro +159 -0
  65. package/src/layouts/SectionHeader.astro +58 -0
  66. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  67. package/src/layouts/ThemeToggle.astro +110 -0
  68. package/src/layouts/astro-shim.d.ts +29 -0
  69. package/src/layouts/index.ts +24 -0
  70. package/src/lib/display.ts +118 -0
  71. package/src/lib/format.ts +65 -0
  72. package/src/lib/index.ts +26 -0
  73. package/src/lib/lenses.ts +241 -0
  74. package/src/lib/repo.ts +44 -0
  75. package/src/lib/scores.ts +94 -0
  76. package/src/lib/search.ts +481 -0
  77. package/src/lib/taxonomy-counts.ts +60 -0
  78. package/src/styles.css +366 -638
  79. package/templates/default/.astro/content.d.ts +155 -0
  80. package/templates/default/.astro/settings.json +5 -0
  81. package/templates/default/.astro/types.d.ts +2 -0
  82. package/templates/default/astro.config.mjs +8 -6
  83. package/templates/default/content/pages/about.md +66 -0
  84. package/templates/default/content/pages/methodology.md +103 -0
  85. package/templates/default/data/generated/records.full.json +8 -0
  86. package/templates/default/data/generated/records.index.json +7 -0
  87. package/templates/default/data/generated/records.json +8 -0
  88. package/templates/default/data/generated/site-config.json +31 -0
  89. package/templates/default/data/records/.gitkeep +0 -0
  90. package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +1 -0
  91. package/templates/default/dist/about/index.html +165 -0
  92. package/templates/default/dist/icons/platforms/android.svg +1 -0
  93. package/templates/default/dist/icons/platforms/apple-dark.svg +1 -0
  94. package/templates/default/dist/icons/platforms/apple-light.svg +1 -0
  95. package/templates/default/dist/icons/platforms/chrome.svg +1 -0
  96. package/templates/default/dist/icons/platforms/chromeos.svg +5 -0
  97. package/templates/default/dist/icons/platforms/desktop.svg +4 -0
  98. package/templates/default/dist/icons/platforms/embedded.svg +5 -0
  99. package/templates/default/dist/icons/platforms/ios.svg +1 -0
  100. package/templates/default/dist/icons/platforms/linux.svg +1 -0
  101. package/templates/default/dist/icons/platforms/macos.svg +1 -0
  102. package/templates/default/dist/icons/platforms/ubuntu.svg +1 -0
  103. package/templates/default/dist/icons/platforms/web.svg +5 -0
  104. package/templates/default/dist/icons/platforms/windows.svg +6 -0
  105. package/templates/default/dist/icons/stacks/android.svg +1 -0
  106. package/templates/default/dist/icons/stacks/apple-dark.svg +1 -0
  107. package/templates/default/dist/icons/stacks/apple-light.svg +1 -0
  108. package/templates/default/dist/icons/stacks/capacitor.svg +14 -0
  109. package/templates/default/dist/icons/stacks/dart.svg +1 -0
  110. package/templates/default/dist/icons/stacks/firebase.svg +1 -0
  111. package/templates/default/dist/icons/stacks/flutter.svg +1 -0
  112. package/templates/default/dist/icons/stacks/graphql.svg +1 -0
  113. package/templates/default/dist/icons/stacks/ionic.svg +4 -0
  114. package/templates/default/dist/icons/stacks/java.svg +1 -0
  115. package/templates/default/dist/icons/stacks/javascript.svg +1 -0
  116. package/templates/default/dist/icons/stacks/kotlin.svg +1 -0
  117. package/templates/default/dist/icons/stacks/mongodb.svg +1 -0
  118. package/templates/default/dist/icons/stacks/nodejs.svg +1 -0
  119. package/templates/default/dist/icons/stacks/python.svg +1 -0
  120. package/templates/default/dist/icons/stacks/react-native.svg +1 -0
  121. package/templates/default/dist/icons/stacks/react.svg +1 -0
  122. package/templates/default/dist/icons/stacks/rust.svg +1 -0
  123. package/templates/default/dist/icons/stacks/solidity.svg +1 -0
  124. package/templates/default/dist/icons/stacks/swift.svg +1 -0
  125. package/templates/default/dist/icons/stacks/tensorflow.svg +1 -0
  126. package/templates/default/dist/icons/stacks/typescript.svg +1 -0
  127. package/templates/default/dist/index.html +55 -0
  128. package/templates/default/dist/llms-full.txt +14 -0
  129. package/templates/default/dist/llms.txt +11 -0
  130. package/templates/default/dist/og-image.svg +48 -0
  131. package/templates/default/dist/projects/index.html +61 -0
  132. package/templates/default/dist/robots.txt +53 -0
  133. package/templates/default/dist/sitemap.xml +21 -0
  134. package/templates/default/dist/submit/index.html +69 -0
  135. package/templates/default/grove.config.ts +36 -0
  136. package/templates/default/node_modules/.bin/astro +21 -0
  137. package/templates/default/node_modules/.bin/astro-check +21 -0
  138. package/templates/default/node_modules/.bin/grove +21 -0
  139. package/templates/default/node_modules/.bin/jiti +21 -0
  140. package/templates/default/node_modules/.bin/prettier +21 -0
  141. package/templates/default/node_modules/.bin/rollup +21 -0
  142. package/templates/default/node_modules/.bin/tsc +21 -0
  143. package/templates/default/node_modules/.bin/tsserver +21 -0
  144. package/templates/default/node_modules/.bin/tsx +21 -0
  145. package/templates/default/node_modules/.bin/vite +21 -0
  146. package/templates/default/node_modules/.bin/yaml +21 -0
  147. package/templates/default/node_modules/.vite/deps/_metadata.json +79 -0
  148. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +433 -0
  149. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +7 -0
  150. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +5 -0
  151. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
  152. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +5 -0
  153. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
  154. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +9 -0
  155. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
  156. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +241 -0
  157. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +7 -0
  158. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +1592 -0
  159. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +7 -0
  160. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  161. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  162. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +186 -0
  163. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +7 -0
  164. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +48 -0
  165. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +7 -0
  166. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +6779 -0
  167. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +7 -0
  168. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +33 -0
  169. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +7 -0
  170. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +88 -0
  171. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +7 -0
  172. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +72 -0
  173. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +7 -0
  174. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +3757 -0
  175. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +7 -0
  176. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +54 -0
  177. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +7 -0
  178. package/templates/default/node_modules/.vite/deps/package.json +3 -0
  179. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +213 -0
  180. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +7 -0
  181. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +525 -0
  182. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +7 -0
  183. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +1020 -0
  184. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +7 -0
  185. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +147 -0
  186. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +7 -0
  187. package/templates/default/package.json +6 -5
  188. package/templates/default/public/llms-full.txt +14 -0
  189. package/templates/default/public/llms.txt +11 -0
  190. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  191. package/templates/default/src/components/layout/Footer.astro +16 -134
  192. package/templates/default/src/components/layout/Header.astro +25 -126
  193. package/templates/default/src/data/records.ts +161 -0
  194. package/templates/default/src/lib/markdown.ts +171 -0
  195. package/templates/default/src/pages/about.astro +95 -429
  196. package/templates/default/src/pages/index.astro +258 -61
  197. package/templates/default/src/pages/projects/[slug].astro +480 -0
  198. package/templates/default/src/pages/projects/index.astro +193 -0
  199. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  200. package/templates/default/src/pages/submit.astro +122 -411
  201. package/templates/default/src/styles/global.css +42 -68
  202. package/src/components/ActiveFilterChips.astro +0 -20
  203. package/src/components/DirectoryFilters.astro +0 -100
  204. package/src/components/DirectoryHero.astro +0 -32
  205. package/src/components/HealthBadge.astro +0 -12
  206. package/src/components/LensTabs.astro +0 -26
  207. package/src/components/MethodologyPanel.astro +0 -20
  208. package/src/components/Pagination.astro +0 -24
  209. package/src/components/ProjectDetail.astro +0 -166
  210. package/src/components/SubmitDraft.astro +0 -137
  211. package/templates/default/src/components/AppCard.astro +0 -248
  212. package/templates/default/src/components/AppSection.astro +0 -58
  213. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  214. package/templates/default/src/components/AppsPagination.astro +0 -147
  215. package/templates/default/src/components/CategoryGrid.astro +0 -80
  216. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  217. package/templates/default/src/components/DecisionRow.astro +0 -219
  218. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  219. package/templates/default/src/components/ExploreByStack.astro +0 -102
  220. package/templates/default/src/components/Hero.astro +0 -176
  221. package/templates/default/src/components/MinimalAbout.astro +0 -57
  222. package/templates/default/src/components/OriginalCollection.astro +0 -144
  223. package/templates/default/src/components/RefinePanel.astro +0 -272
  224. package/templates/default/src/components/ScoreBars.astro +0 -72
  225. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  226. package/templates/default/src/components/StackGrid.astro +0 -95
  227. package/templates/default/src/components/WhyThisExists.astro +0 -81
  228. package/templates/default/src/components/icons/Icon.astro +0 -148
  229. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  230. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  231. package/templates/default/src/data/apps.ts +0 -149
  232. package/templates/default/src/data/categories.ts +0 -27
  233. package/templates/default/src/data/config.ts +0 -28
  234. package/templates/default/src/data/contributors.ts +0 -96
  235. package/templates/default/src/data/stacks.ts +0 -47
  236. package/templates/default/src/data/stats.ts +0 -65
  237. package/templates/default/src/data/types.ts +0 -248
  238. package/templates/default/src/lib/apps-search.ts +0 -228
  239. package/templates/default/src/lib/format.ts +0 -36
  240. package/templates/default/src/lib/lenses.ts +0 -98
  241. package/templates/default/src/lib/repo.ts +0 -12
  242. package/templates/default/src/lib/scores.ts +0 -51
  243. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  244. package/templates/default/src/pages/apps/[slug].astro +0 -709
  245. package/templates/default/src/pages/apps/index.astro +0 -669
  246. package/templates/default/src/pages/contributors.astro +0 -142
@@ -0,0 +1,480 @@
1
+ ---
2
+ /**
3
+ * Detail page — one record at a time.
4
+ *
5
+ * Layout (top to bottom):
6
+ * 1. Breadcrumb — back to /projects (or blueprint slug)
7
+ * 2. AppCard-style header — logo + name + owner + description + CTAs
8
+ * 3. ScoreBars — 5-dimension + overall score block
9
+ * 4. BestFor / WhyListed / Caveats — 3-column curation grid
10
+ * 5. GitHub signals dl — stars, forks, pushed, license, language
11
+ * 6. Stack + Platform chips — Icon (with fallback to text)
12
+ * 7. Distribution channels — npm/crates/docker/etc. (when present)
13
+ * 8. Content markdown — `record.content` (rendered as raw HTML
14
+ * body) when present
15
+ * 9. JSON-LD SoftwareSourceCode — for SEO + AI crawlers
16
+ *
17
+ * The page is generic across the 3 record kinds (project / resource
18
+ * / entity) — non-project fields simply render nothing.
19
+ */
20
+ import siteConfig from "../../../data/generated/site-config.json";
21
+ import { fullRecords, projectBySlug, resourceBySlug, entityBySlug, indexSlug } from "../../data/records";
22
+ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
23
+ import Icon from "@grove-dev/astro/components/Icon.astro";
24
+ import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
25
+ import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
26
+ import { formatRelative, formatStars } from "@grove-dev/astro";
27
+ import { statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
28
+ import { readFileSync, existsSync } from "node:fs";
29
+ import { fileURLToPath } from "node:url";
30
+ import { dirname, resolve } from "node:path";
31
+ import type { ProjectRecord } from "@grove-dev/core";
32
+
33
+ export function getStaticPaths() {
34
+ return fullRecords.map((record) => ({
35
+ params: { slug: record.slug },
36
+ }));
37
+ }
38
+
39
+ const slug = indexSlug();
40
+ const recordSlug = Astro.params.slug ?? "";
41
+ const project = projectBySlug(recordSlug);
42
+ const resource = resourceBySlug(recordSlug);
43
+ const entity = entityBySlug(recordSlug);
44
+ const record = project ?? resource ?? entity;
45
+
46
+ if (!record) {
47
+ return Astro.redirect(`/${slug}`);
48
+ }
49
+
50
+ const isProject = record.kind === "project";
51
+ const proj: ProjectRecord | null = project;
52
+
53
+ const name =
54
+ record.kind === "resource" ? (record as { title: string }).title : (record as { name: string }).name;
55
+
56
+ const title = `${name} — ${siteConfig.name}`;
57
+ const description = record.description ?? `${name} on ${siteConfig.name}`;
58
+
59
+ const repoUrl = proj?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
60
+ const homepageUrl = record.links?.website ?? "";
61
+ const logoUrl = proj?.logoUrl;
62
+ const stacks = proj?.stacks ?? (proj?.stack ? [proj.stack] : []);
63
+ const platforms = proj?.platforms ?? [];
64
+ const github = proj?.github?.repository;
65
+ const stars = github?.stargazers_count ?? 0;
66
+ const forks = github?.forks_count ?? 0;
67
+ const language = github?.language ?? null;
68
+ const licenseSpdx = github?.license?.spdx_id ?? null;
69
+ const pushedAt = github?.pushed_at ?? null;
70
+ const starsLabel = formatStars(stars);
71
+ const pushedLabel = formatRelative(pushedAt);
72
+
73
+ const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
74
+ const avatarSrc = logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
75
+ const initials = (name || "").replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase();
76
+
77
+ const bestFor = proj?.bestFor ?? [];
78
+ const whyListed = proj?.whyListed ?? [];
79
+ const caveats = proj?.caveats ?? [];
80
+ const distribution = proj?.distribution?.channels ?? [];
81
+ const scores = proj?.scores ?? null;
82
+ const curationLabels = record.curation?.labels ?? [];
83
+ const tags = record.tags ?? [];
84
+ const healthStatus = proj?.health?.status;
85
+ const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
86
+
87
+ // ── Optional content markdown (record.content is a path under
88
+ // content/records/<slug>.md) ────────────────────────────────────
89
+ let contentHtml: string | null = null;
90
+ if (isProject && proj?.content) {
91
+ const here = dirname(fileURLToPath(import.meta.url));
92
+ const candidates = [
93
+ resolve(here, "..", "..", "..", proj.content),
94
+ resolve(process.cwd(), proj.content),
95
+ ];
96
+ const contentPath = candidates.find((p) => existsSync(p));
97
+ if (contentPath) {
98
+ try {
99
+ // Cheap markdown → HTML using a minimal renderer would be
100
+ // nice, but we don't want to pull another dep into the
101
+ // template. Render the file as a <pre> for now so the body
102
+ // still surfaces on the page. Consumers can replace this
103
+ // with a real markdown pipeline (`marked`, `remark`, ...)
104
+ // by overriding the `[slug].astro` page.
105
+ const text = readFileSync(contentPath, "utf8");
106
+ contentHtml = text
107
+ .replace(/&/g, "&amp;")
108
+ .replace(/</g, "&lt;")
109
+ .replace(/>/g, "&gt;");
110
+ } catch {
111
+ contentHtml = null;
112
+ }
113
+ }
114
+ }
115
+
116
+ // ── JSON-LD (SoftwareSourceCode) for project-kind records ──────
117
+ let jsonLd: Record<string, unknown> | null = null;
118
+ if (isProject && proj) {
119
+ jsonLd = {
120
+ "@context": "https://schema.org",
121
+ "@type": "SoftwareSourceCode",
122
+ name,
123
+ description: record.description ?? undefined,
124
+ url: repoUrl || homepageUrl || undefined,
125
+ codeRepository: repoUrl || undefined,
126
+ programmingLanguage: language ?? undefined,
127
+ license: licenseSpdx ?? undefined,
128
+ author: ownerRepo
129
+ ? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
130
+ : undefined,
131
+ };
132
+ }
133
+ ---
134
+
135
+ <BaseLayout
136
+ title={title}
137
+ description={description}
138
+ site={siteConfig}
139
+ type="article"
140
+ jsonLd={jsonLd ?? undefined}
141
+ submitHref="/submit"
142
+ submitLabel={`Submit ${name}`}
143
+ >
144
+ <article class="grove-section">
145
+ <div class="grove-container-wide mx-auto px-5 sm:px-7">
146
+ <nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
147
+ <a href={`/${slug}`} class="hover:text-ink-900 dark:hover:text-ink-100 capitalize">
148
+ {slug}
149
+ </a>
150
+ <span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
151
+ <span class="text-ink-700 dark:text-ink-300">{name}</span>
152
+ </nav>
153
+
154
+ {/* ── AppCard-style header ──────────────────────────────── */}
155
+ <header class="grove-card flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
156
+ <div class="flex-shrink-0">
157
+ {avatarSrc ? (
158
+ <img
159
+ src={avatarSrc}
160
+ alt=""
161
+ width="64"
162
+ height="64"
163
+ loading="lazy"
164
+ decoding="async"
165
+ class="h-16 w-16 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950"
166
+ />
167
+ ) : (
168
+ <span
169
+ aria-hidden="true"
170
+ class="inline-flex h-16 w-16 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-2xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
171
+ >
172
+ {initials || "·"}
173
+ </span>
174
+ )}
175
+ </div>
176
+
177
+ <div class="min-w-0 flex-1">
178
+ <div class="flex flex-wrap items-center gap-2">
179
+ <span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:text-ink-300">
180
+ {record.kind}
181
+ </span>
182
+ {record.category && (
183
+ <span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
184
+ {record.category}
185
+ </span>
186
+ )}
187
+ {curationLabels.map((l) => (
188
+ <span class="grove-badge grove-badge-{l}" title={`Curated: ${labelDisplay(l) ?? l}`}>
189
+ {labelDisplay(l) ?? prettySlug(l)}
190
+ </span>
191
+ ))}
192
+ {healthLabel && (
193
+ <span class="grove-badge grove-badge-stale">{healthLabel}</span>
194
+ )}
195
+ </div>
196
+
197
+ <h1 class="m-0 mt-2 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
198
+ {name}
199
+ </h1>
200
+
201
+ {ownerRepo && (
202
+ <p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
203
+ <a
204
+ href={`https://github.com/${ownerRepo.owner}`}
205
+ target="_blank"
206
+ rel="noopener noreferrer"
207
+ class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
208
+ >
209
+ @{ownerRepo.owner}
210
+ </a>
211
+ <span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
212
+ <a
213
+ href={repoUrl}
214
+ target="_blank"
215
+ rel="noopener noreferrer"
216
+ class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
217
+ >
218
+ {ownerRepo.repo}
219
+ </a>
220
+ </p>
221
+ )}
222
+
223
+ {record.description && (
224
+ <p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
225
+ {record.description}
226
+ </p>
227
+ )}
228
+
229
+ <div class="mt-5 flex flex-wrap gap-2">
230
+ {repoUrl && (
231
+ <a
232
+ href={repoUrl}
233
+ target="_blank"
234
+ rel="noopener noreferrer"
235
+ class="grove-btn grove-btn-primary"
236
+ >
237
+ View on GitHub →
238
+ </a>
239
+ )}
240
+ {homepageUrl && (
241
+ <a
242
+ href={homepageUrl}
243
+ target="_blank"
244
+ rel="noopener noreferrer"
245
+ class="grove-btn grove-btn-outline"
246
+ >
247
+ Homepage
248
+ </a>
249
+ )}
250
+ </div>
251
+ </div>
252
+ </header>
253
+
254
+ {/* ── Score bars ───────────────────────────────────────── */}
255
+ {proj && (
256
+ <section class="mt-8 grove-card p-6" aria-labelledby="scores-heading">
257
+ <h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
258
+ Scores
259
+ </h2>
260
+ <p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
261
+ A five-dimension read of how likely this project is to satisfy a curious developer.
262
+ </p>
263
+ <div class="mt-4">
264
+ <ScoreBars scores={scores} />
265
+ </div>
266
+ </section>
267
+ )}
268
+
269
+ {/* ── BestFor / WhyListed / Caveats grid ──────────────── */}
270
+ {isProject && (bestFor.length > 0 || whyListed.length > 0 || caveats.length > 0) && (
271
+ <section class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3" aria-label="Curation notes">
272
+ {bestFor.length > 0 && (
273
+ <div class="grove-card p-5">
274
+ <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
275
+ Best for
276
+ </h3>
277
+ <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
278
+ {bestFor.map((b) => (
279
+ <li class="text-sm text-ink-700 dark:text-ink-200">— {b}</li>
280
+ ))}
281
+ </ul>
282
+ </div>
283
+ )}
284
+ {whyListed.length > 0 && (
285
+ <div class="grove-card p-5">
286
+ <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
287
+ Why listed
288
+ </h3>
289
+ <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
290
+ {whyListed.map((w) => (
291
+ <li class="text-sm text-ink-700 dark:text-ink-200">— {w}</li>
292
+ ))}
293
+ </ul>
294
+ </div>
295
+ )}
296
+ {caveats.length > 0 && (
297
+ <div class="grove-card p-5">
298
+ <h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
299
+ Caveats
300
+ </h3>
301
+ <ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
302
+ {caveats.map((c) => (
303
+ <li class="text-sm text-ink-700 dark:text-ink-200">— {c}</li>
304
+ ))}
305
+ </ul>
306
+ </div>
307
+ )}
308
+ </section>
309
+ )}
310
+
311
+ {/* ── GitHub signals dl ────────────────────────────────── */}
312
+ {isProject && github && (
313
+ <section class="mt-8 grove-card p-6" aria-labelledby="signals-heading">
314
+ <h2 id="signals-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
315
+ GitHub signals
316
+ </h2>
317
+ <dl
318
+ class="mt-4 grid grid-cols-2 gap-x-6 gap-y-3 text-sm sm:grid-cols-3"
319
+ aria-label="Repository signals"
320
+ >
321
+ <div>
322
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
323
+ Stars
324
+ </dt>
325
+ <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
326
+ {starsLabel ?? "—"}
327
+ </dd>
328
+ </div>
329
+ <div>
330
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
331
+ Forks
332
+ </dt>
333
+ <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
334
+ {forks}
335
+ </dd>
336
+ </div>
337
+ <div>
338
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
339
+ Last push
340
+ </dt>
341
+ <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
342
+ {pushedLabel}
343
+ </dd>
344
+ </div>
345
+ {language && (
346
+ <div>
347
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
348
+ Language
349
+ </dt>
350
+ <dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{language}</dd>
351
+ </div>
352
+ )}
353
+ {licenseSpdx && (
354
+ <div>
355
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
356
+ License
357
+ </dt>
358
+ <dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
359
+ </div>
360
+ )}
361
+ {pushedAt && (
362
+ <div>
363
+ <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
364
+ Updated
365
+ </dt>
366
+ <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
367
+ {pushedAt.slice(0, 10)}
368
+ </dd>
369
+ </div>
370
+ )}
371
+ </dl>
372
+ </section>
373
+ )}
374
+
375
+ {/* ── Stack + Platform chips ───────────────────────────── */}
376
+ {isProject && (stacks.length > 0 || platforms.length > 0) && (
377
+ <section class="mt-8" aria-labelledby="stacks-heading">
378
+ <h2 id="stacks-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
379
+ Stack & platforms
380
+ </h2>
381
+ <div class="mt-4 flex flex-col gap-3">
382
+ {stacks.length > 0 && (
383
+ <div class="flex flex-wrap items-center gap-2">
384
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
385
+ Stack
386
+ </span>
387
+ {stacks.map((s) => (
388
+ <span class="grove-badge inline-flex items-center gap-1.5">
389
+ <Icon name={s} category="stack" size={14} />
390
+ {s}
391
+ </span>
392
+ ))}
393
+ </div>
394
+ )}
395
+ {platforms.length > 0 && (
396
+ <div class="flex flex-wrap items-center gap-2">
397
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
398
+ Platform
399
+ </span>
400
+ {platforms.map((p) => (
401
+ <span class="grove-badge inline-flex items-center gap-1.5">
402
+ <Icon name={p} category="platform" size={14} />
403
+ {p}
404
+ </span>
405
+ ))}
406
+ </div>
407
+ )}
408
+ </div>
409
+ </section>
410
+ )}
411
+
412
+ {/* ── Tags ─────────────────────────────────────────────── */}
413
+ {tags.length > 0 && (
414
+ <section class="mt-8" aria-labelledby="tags-heading">
415
+ <h2 id="tags-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
416
+ Tags
417
+ </h2>
418
+ <ul class="mt-4 m-0 flex list-none flex-wrap gap-1.5 p-0">
419
+ {tags.map((t) => (
420
+ <li>
421
+ <a
422
+ href={`/${slug}?q=${encodeURIComponent(t)}`}
423
+ class="grove-badge inline-flex items-center gap-1.5"
424
+ >
425
+ {t}
426
+ </a>
427
+ </li>
428
+ ))}
429
+ </ul>
430
+ </section>
431
+ )}
432
+
433
+ {/* ── Distribution channels ────────────────────────────── */}
434
+ {distribution.length > 0 && (
435
+ <section class="mt-8" aria-labelledby="dist-heading">
436
+ <h2 id="dist-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
437
+ Distribution channels
438
+ </h2>
439
+ <ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
440
+ {distribution.map((c) => (
441
+ <li class="grove-card flex flex-wrap items-baseline justify-between gap-2 p-3">
442
+ <div class="flex flex-col">
443
+ <span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
444
+ {c.label ?? c.type}
445
+ {c.verified && (
446
+ <span class="ml-2 grove-badge grove-badge-new">Verified</span>
447
+ )}
448
+ </span>
449
+ {c.notes && (
450
+ <span class="text-xs text-ink-500 dark:text-ink-400">{c.notes}</span>
451
+ )}
452
+ </div>
453
+ <a
454
+ href={c.url}
455
+ target="_blank"
456
+ rel="noopener noreferrer"
457
+ class="grove-btn grove-btn-outline"
458
+ >
459
+ {c.platform ?? c.type} →
460
+ </a>
461
+ </li>
462
+ ))}
463
+ </ul>
464
+ </section>
465
+ )}
466
+
467
+ {/* ── Content markdown (record.content → <pre> for now) ── */}
468
+ {contentHtml && (
469
+ <section class="mt-8" aria-labelledby="content-heading">
470
+ <h2 id="content-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
471
+ Notes
472
+ </h2>
473
+ <pre
474
+ class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"
475
+ ><code>{contentHtml}</code></pre>
476
+ </section>
477
+ )}
478
+ </div>
479
+ </article>
480
+ </BaseLayout>
@@ -0,0 +1,193 @@
1
+ ---
2
+ /**
3
+ * List page — the discovery / browse page.
4
+ *
5
+ * Renders the full filter / search / sort machinery:
6
+ * - SmartLensTabs (All / Trending / Recently added / Established / Featured / Needs review)
7
+ * - RefinePanel (Stack / Platform / Category / License / Sort multi-selects)
8
+ * - AppsIndexRow list (compact table-row per record)
9
+ * - AppsPagination (windowed page list)
10
+ * - ExploreByCategory / ExploreByStack (compact pill lists)
11
+ *
12
+ * Pure server-rendered: every interaction is a GET to the same URL
13
+ * with a different query string. No client-side filter script — the
14
+ * page rebuilds on every request because Astro's default output is
15
+ * static (all pages are baked at build time and revalidated by the
16
+ * deploy target).
17
+ */
18
+ import siteConfig from "../../../data/generated/site-config.json";
19
+ import { records, indexSlug, itemLabel } from "../../data/records";
20
+ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
21
+ import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
22
+ import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
23
+ import AppsIndexRow from "@grove-dev/astro/components/AppsIndexRow.astro";
24
+ import AppsPagination from "@grove-dev/astro/components/AppsPagination.astro";
25
+ import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
26
+ import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
27
+ import {
28
+ filtersFromSearchParams,
29
+ filterApps,
30
+ applySort,
31
+ paginate,
32
+ totalPages,
33
+ effectivePage,
34
+ buildFacets,
35
+ PAGE_SIZE,
36
+ pipeline,
37
+ } from "@grove-dev/astro";
38
+
39
+ const slug = indexSlug();
40
+ const total = records.length;
41
+
42
+ const title = `Browse ${slug} — ${siteConfig.name}`;
43
+ const description = `A searchable directory of ${slug} for ${siteConfig.name}.`;
44
+
45
+ // ── URL → filters ───────────────────────────────────────────────
46
+ const filters = filtersFromSearchParams(Astro.url.searchParams);
47
+
48
+ // ── Facet map (built from the FULL record set, not the filtered
49
+ // one, so users can see all available options even when none of
50
+ // them are active) ───────────────────────────────────────────────
51
+ const facets = buildFacets(records);
52
+
53
+ // ── Lens → filter → sort pipeline (matches the openapps list) ──
54
+ const filtered = pipeline(records, filters);
55
+
56
+ // ── Pagination ───────────────────────────────────────────────────
57
+ const page = effectivePage(filters);
58
+ const pages = totalPages(filtered.length, PAGE_SIZE);
59
+ const pageItems = paginate(filtered, page, PAGE_SIZE);
60
+
61
+ // ── "Explore" facet pills (top 12 categories / stacks by count) ─
62
+ const categoryEntries = facets.categories.slice(0, 12).map((f) => ({
63
+ name: f.value,
64
+ slug: f.value,
65
+ count: f.count,
66
+ }));
67
+ const stackEntries = facets.stacks.slice(0, 18).map((f) => ({
68
+ name: f.value,
69
+ slug: f.value,
70
+ count: f.count,
71
+ }));
72
+
73
+ function hrefForPage(p: number): string {
74
+ const sp = new URLSearchParams(Astro.url.searchParams);
75
+ if (p <= 1) sp.delete("page");
76
+ else sp.set("page", String(p));
77
+ const qs = sp.toString();
78
+ return qs ? `/${slug}?${qs}` : `/${slug}`;
79
+ }
80
+ ---
81
+
82
+ <BaseLayout
83
+ title={title}
84
+ description={description}
85
+ site={siteConfig}
86
+ submitHref="/submit"
87
+ submitLabel={`Submit ${itemLabel()}`}
88
+ >
89
+ <section class="grove-section-tight border-b border-ink-200 dark:border-ink-800">
90
+ <div class="grove-container-wide mx-auto px-5 pt-10 sm:px-7 sm:pt-12">
91
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
92
+ {slug}
93
+ </span>
94
+ <h1 class="m-0 mt-1 text-[1.875rem] sm:text-[2.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
95
+ Browse {slug}
96
+ </h1>
97
+ <p class="m-0 mt-2 max-w-[64ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
98
+ Search, filter, and explore {total} {itemLabel()}{total === 1 ? "" : "s"} in this directory.
99
+ </p>
100
+
101
+ <div class="mt-6">
102
+ <SmartLensTabs
103
+ currentParams={Astro.url.searchParams}
104
+ pathPrefix={`/${slug}`}
105
+ />
106
+ </div>
107
+
108
+ <div class="mt-5">
109
+ <RefinePanel
110
+ facets={facets}
111
+ initial={filters}
112
+ pathPrefix={`/${slug}`}
113
+ />
114
+ </div>
115
+ </div>
116
+ </section>
117
+
118
+ {categoryEntries.length > 0 && (
119
+ <ExploreByCategory
120
+ categories={categoryEntries}
121
+ pathPrefix={`/${slug}`}
122
+ />
123
+ )}
124
+
125
+ {stackEntries.length > 0 && (
126
+ <ExploreByStack
127
+ stacks={stackEntries}
128
+ pathPrefix={`/${slug}`}
129
+ />
130
+ )}
131
+
132
+ <section class="grove-section-tight">
133
+ <div class="grove-container-wide mx-auto px-5 sm:px-7">
134
+ <div class="mb-4 flex flex-wrap items-baseline justify-between gap-2">
135
+ <p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
136
+ Showing
137
+ <span class="ml-1 font-mono text-ink-900 dark:text-ink-100">
138
+ {filtered.length === 0 ? 0 : (page - 1) * PAGE_SIZE + 1}
139
+ </span>
140
+
141
+ <span class="font-mono text-ink-900 dark:text-ink-100">
142
+ {Math.min(page * PAGE_SIZE, filtered.length)}
143
+ </span>
144
+ of
145
+ <span class="ml-1 font-mono text-ink-900 dark:text-ink-100">
146
+ {filtered.length}
147
+ </span>
148
+ {itemLabel()}{filtered.length === 1 ? "" : "s"}
149
+ </p>
150
+ {pages > 1 && (
151
+ <AppsPagination
152
+ current={page}
153
+ total={pages}
154
+ hrefFor={hrefForPage}
155
+ />
156
+ )}
157
+ </div>
158
+
159
+ {pageItems.length === 0 ? (
160
+ <div class="flex flex-col items-center gap-2 py-16 text-center">
161
+ <p class="m-0 text-[0.9375rem] font-medium text-ink-700 dark:text-ink-200">
162
+ No {itemLabel()}s match these filters.
163
+ </p>
164
+ <a
165
+ href={`/${slug}`}
166
+ class="mt-3 text-sm text-ink-500 hover:text-ink-900 dark:hover:text-ink-100"
167
+ >
168
+ Clear all filters
169
+ </a>
170
+ </div>
171
+ ) : (
172
+ <div class="flex flex-col gap-3">
173
+ {pageItems.map((record) => (
174
+ <AppsIndexRow
175
+ record={record}
176
+ detailHref={`/${slug}/${record.slug}`}
177
+ />
178
+ ))}
179
+ </div>
180
+ )}
181
+
182
+ {pages > 1 && (
183
+ <div class="mt-6 flex justify-center">
184
+ <AppsPagination
185
+ current={page}
186
+ total={pages}
187
+ hrefFor={hrefForPage}
188
+ />
189
+ </div>
190
+ )}
191
+ </div>
192
+ </section>
193
+ </BaseLayout>