@grove-dev/astro 0.1.4 → 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
@@ -1,144 +1,43 @@
1
1
  ---
2
- import { siteConfig } from "../../data/config";
3
- import ThemeToggle from "./ThemeToggle.astro";
4
- import { stats } from "../../data/stats";
5
- import { compact } from "../../lib/format";
6
-
7
- const githubUrl = stats.originalRepo;
8
-
9
- // Nav items. Order is information-architecture: discover → browse → act.
10
- // `Contributors` is intentionally not a top-level item — it lives lower
11
- // on the page. The watchlist was removed in v0.1 (see CHANGELOG).
12
- //
13
- // All links point to /apps with a filter rather than in-page anchors.
14
- // This keeps the nav usable from any page (e.g. /about) — anchor links
15
- // would only work on the home page, leaving /apps and /about with dead
16
- // links. Category/Stack filters are exposed via the /apps refine panel
17
- // and the home page's "Browse by stack"/"Browse by category" sections.
18
- const navItems = [
19
- { label: "New", href: "/apps?label=new" },
20
- { label: "Hot", href: "/apps?label=hot" },
21
- { label: "Categories", href: "/apps" },
22
- { label: "Stacks", href: "/apps" },
23
- { label: "About", href: "/about" },
24
- ];
25
-
26
- const starsLabel = compact(stats.stars);
2
+ import siteConfig from "../../data/generated/site-config.json";
3
+ import { indexSlug } from "../../data/records";
4
+
5
+ const navItems = siteConfig.nav.length > 0
6
+ ? siteConfig.nav
7
+ : [
8
+ { label: "Home", href: "/" },
9
+ { label: "Browse", href: `/${indexSlug()}` },
10
+ { label: "About", href: "/about" },
11
+ { label: "Submit", href: "/submit" },
12
+ ];
27
13
  ---
28
14
 
29
- <header
30
- class="sticky top-0 z-50 border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85"
31
- >
32
- <div
33
- class="mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
34
- >
15
+ <header class="border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85">
16
+ <div class="mx-auto flex max-w-6xl items-center gap-6 px-5 py-3 sm:px-7">
35
17
  <a
36
18
  href="/"
37
- class="inline-flex items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
19
+ class="inline-flex items-center gap-2 text-base font-semibold tracking-tight text-ink-900 dark:text-ink-100"
38
20
  >
39
21
  <span
40
22
  aria-hidden="true"
41
- class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
23
+ class="inline-block h-4 w-4 rounded bg-ink-900 dark:bg-ink-100"
42
24
  ></span>
43
25
  <span>{siteConfig.name}</span>
44
26
  </a>
45
27
 
46
28
  <nav class="hidden flex-1 md:block" aria-label="Primary">
47
29
  <ul class="m-0 flex list-none gap-5 p-0">
48
- {
49
- navItems.map((item) => (
50
- <li>
51
- <a
52
- href={item.href}
53
- class="text-[0.875rem] font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
54
- >
55
- {item.label}
56
- </a>
57
- </li>
58
- ))
59
- }
30
+ {navItems.map((item) => (
31
+ <li>
32
+ <a
33
+ href={item.href}
34
+ class="text-sm font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
35
+ >
36
+ {item.label}
37
+ </a>
38
+ </li>
39
+ ))}
60
40
  </ul>
61
41
  </nav>
62
-
63
- <div class="ml-auto flex items-center gap-2">
64
- <a
65
- href="/submit"
66
- 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"
67
- >
68
- <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
69
- <path d="M7.75 2a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 7.75 2Z"/>
70
- </svg>
71
- <span>Submit app</span>
72
- </a>
73
-
74
- {/* GitHub button. The star count is rendered as text, not an
75
- external badge image, so it works offline, doesn't depend
76
- on shields.io, and reflects what we already know (stats.stars). */}
77
- <a
78
- href={githubUrl}
79
- target="_blank"
80
- rel="noopener noreferrer"
81
- aria-label={`View source on GitHub — ${stats.stars} stars`}
82
- title="View source on GitHub"
83
- 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"
84
- >
85
- <span class="flex items-center gap-1.5 px-2.5 py-1.5 text-ink-900 dark:text-ink-100">
86
- <svg
87
- viewBox="0 0 16 16"
88
- width="16"
89
- height="16"
90
- aria-hidden="true"
91
- fill="currentColor"
92
- >
93
- <path
94
- 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"
95
- />
96
- </svg>
97
- <span>Star</span>
98
- </span>
99
- <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">
100
- <svg
101
- viewBox="0 0 16 16"
102
- width="11"
103
- height="11"
104
- aria-hidden="true"
105
- fill="currentColor"
106
- class="text-amber-500"
107
- >
108
- <path
109
- 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"
110
- />
111
- </svg>
112
- <span class="font-mono">{starsLabel}</span>
113
- </span>
114
- </a>
115
- <ThemeToggle />
116
- </div>
117
42
  </div>
118
43
  </header>
119
-
120
- <script>
121
- // Smooth-scroll for in-page anchor links (Header nav + Hero CTA).
122
- // Works on any <a href="#…"> that points to an existing element on
123
- // the current page. External links (e.g. /about, /apps?…) are skipped
124
- // because they don't start with "#". Modifier-clicks (Cmd/Ctrl/Shift/
125
- // middle) let the browser handle the anchor normally so users can
126
- // open the same section in a new tab.
127
- document.addEventListener("click", (e) => {
128
- if (e.defaultPrevented) return;
129
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
130
- if (e.button !== 0) return; // non-primary mouse button
131
- const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
132
- 'a[href^="#"]',
133
- );
134
- if (!link) return;
135
- const id = link.getAttribute("href")!;
136
- if (id.length <= 1) return; // bare "#" → ignore
137
- const target = document.querySelector(id);
138
- if (!target) return; // not on this page; let the browser navigate
139
- e.preventDefault();
140
- target.scrollIntoView({ behavior: "smooth", block: "start" });
141
- // Update the URL hash without an extra jump, so back/forward works.
142
- history.replaceState(null, "", id);
143
- });
144
- </script>
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Source of truth: `data/generated/records.{full,index,json}`,
3
+ * produced at build time by `grove generate` from
4
+ * `data/records/*.yml`.
5
+ *
6
+ * Three flavors are written by the generator:
7
+ *
8
+ * - `records.full.json` — every record, all visibility. Carries
9
+ * every normalized field (including `content`, `bestFor`,
10
+ * `whyListed`, `caveats`, full `github.repository`, ...).
11
+ * Use this for the detail page.
12
+ * - `records.index.json` — slim projection, visible-only. Use
13
+ * this for the list page and any home-page sectioning.
14
+ * - `records.json` — alias of `records.full.json`.
15
+ *
16
+ * The YML files are the human-edited source; this module is a
17
+ * typed re-export so pages can import the records without parsing
18
+ * JSON inline. The shape is the V1 discriminated union from
19
+ * `@grove-dev/core` — records have a `kind` field that selects
20
+ * `ProjectRecord`, `ResourceRecord`, or `EntityRecord`. Pages cast
21
+ * to the kind they expect based on the blueprint in
22
+ * `grove.config.ts`.
23
+ *
24
+ * When the JSON files are missing (e.g. before `grove generate`
25
+ * runs, or in a fresh scaffold with no records) this module falls
26
+ * back to empty arrays so the Astro build still succeeds — every
27
+ * page must render a graceful "no records yet" state.
28
+ */
29
+ import { existsSync, readFileSync } from "node:fs";
30
+ import { fileURLToPath } from "node:url";
31
+ import { dirname, resolve } from "node:path";
32
+ import type { ProjectRecord, ResourceRecord, EntityRecord, Resource } from "@grove-dev/core";
33
+
34
+ interface RecordsPayload {
35
+ schemaVersion?: number;
36
+ blueprint?: string;
37
+ generatedAt?: string;
38
+ totalRecords?: number;
39
+ visibleRecords?: number;
40
+ records?: Resource[];
41
+ }
42
+
43
+ function loadGenerated(filename: string): Resource[] {
44
+ // The Astro build resolves JSON imports from `src/data/*.ts`, but
45
+ // we want to be defensive about the order of operations: a fresh
46
+ // scaffold runs `astro build` without first running
47
+ // `grove generate`, in which case the JSON file does not exist.
48
+ // Resolve the path relative to this module's URL, then fall back
49
+ // to an empty list.
50
+ try {
51
+ const here = dirname(fileURLToPath(import.meta.url));
52
+ const candidates = [
53
+ resolve(here, "..", "..", "data", "generated", filename),
54
+ resolve(here, "..", "..", "..", "data", "generated", filename),
55
+ resolve(process.cwd(), "data", "generated", filename),
56
+ ];
57
+ const path = candidates.find((p) => existsSync(p));
58
+ if (!path) return [];
59
+ const raw = readFileSync(path, "utf8");
60
+ const parsed = JSON.parse(raw) as RecordsPayload;
61
+ return (parsed.records ?? []) as Resource[];
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ const fullRecordsRaw = loadGenerated("records.full.json");
68
+ const indexRecordsRaw = loadGenerated("records.index.json");
69
+
70
+ /**
71
+ * Full records (all visibility). Use this for the detail page,
72
+ * where you need `content`, `bestFor`, `whyListed`, `caveats`,
73
+ * the full `github.repository` block, etc.
74
+ */
75
+ export const fullRecords: Resource[] = fullRecordsRaw;
76
+
77
+ /**
78
+ * Index-payload records (visible only). Use this for the list
79
+ * page and any home-page sectioning, where you only need the
80
+ * slim search-index fields.
81
+ */
82
+ export const records: Resource[] =
83
+ indexRecordsRaw.length > 0 ? indexRecordsRaw : fullRecordsRaw;
84
+
85
+ /** Project-kind records only. */
86
+ export const projects = records.filter(
87
+ (r): r is ProjectRecord => r.kind === "project",
88
+ );
89
+
90
+ /** Resource-kind records. */
91
+ export const resources = records.filter(
92
+ (r): r is ResourceRecord => r.kind === "resource",
93
+ );
94
+
95
+ /** Entity-kind records. */
96
+ export const entities = records.filter(
97
+ (r): r is EntityRecord => r.kind === "entity",
98
+ );
99
+
100
+ const bySlug = new Map(fullRecords.map((r) => [r.slug, r]));
101
+
102
+ export function recordBySlug(slug: string): Resource | undefined {
103
+ return bySlug.get(slug);
104
+ }
105
+
106
+ /**
107
+ * Generic slug lookup. The spec name is `findRecord`; this is an
108
+ * alias of `recordBySlug` so consumers can use either spelling.
109
+ * The two-name pattern matches the rest of the module (we keep
110
+ * `recordBySlug` for the typed `projectBySlug`/`resourceBySlug`/
111
+ * `entityBySlug` family, and `findRecord` as the unified entry
112
+ * point).
113
+ */
114
+ export function findRecord(slug: string): Resource | undefined {
115
+ return bySlug.get(slug);
116
+ }
117
+
118
+ export function projectBySlug(slug: string): ProjectRecord | undefined {
119
+ const r = bySlug.get(slug);
120
+ return r && r.kind === "project" ? r : undefined;
121
+ }
122
+
123
+ export function resourceBySlug(slug: string): ResourceRecord | undefined {
124
+ const r = bySlug.get(slug);
125
+ return r && r.kind === "resource" ? r : undefined;
126
+ }
127
+
128
+ export function entityBySlug(slug: string): EntityRecord | undefined {
129
+ const r = bySlug.get(slug);
130
+ return r && r.kind === "entity" ? r : undefined;
131
+ }
132
+
133
+ /**
134
+ * Page slug for the directory index page, derived from the
135
+ * blueprint in `grove.config.ts` (mirrors the helper that used
136
+ * to live in `data/site-config.ts`).
137
+ */
138
+ export function indexSlug(blueprint: string | undefined): string {
139
+ switch (blueprint) {
140
+ case "resource-hub":
141
+ return "resources";
142
+ case "ecosystem-map":
143
+ return "entities";
144
+ case "project-directory":
145
+ default:
146
+ return "projects";
147
+ }
148
+ }
149
+
150
+ /** Human label for items in the UI. */
151
+ export function itemLabel(blueprint: string | undefined): string {
152
+ switch (blueprint) {
153
+ case "resource-hub":
154
+ return "resource";
155
+ case "ecosystem-map":
156
+ return "entity";
157
+ case "project-directory":
158
+ default:
159
+ return "project";
160
+ }
161
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tiny markdown → HTML converter for page-level content bodies.
3
+ *
4
+ * Scope is intentionally narrow: it handles the markdown the
5
+ * `content/pages/*.md` files actually use (headings, paragraphs,
6
+ * unordered + ordered lists, fenced code, inline `code` and
7
+ * links). Anything more elaborate (tables, footnotes, autolinks)
8
+ * is out of scope — consumers can swap in a real renderer
9
+ * (marked, remark, ...) by overriding the page-level helper.
10
+ *
11
+ * The output is HTML-escaped at the source so a stray `<` in the
12
+ * markdown file can't break the page. Inline `code` and links are
13
+ * processed before the escape pass.
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+
17
+ function escapeHtml(s: string): string {
18
+ return s
19
+ .replace(/&/g, "&amp;")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;");
22
+ }
23
+
24
+ function renderInline(line: string): string {
25
+ let out = escapeHtml(line);
26
+ // Inline code: `…` — render first so its content doesn't get
27
+ // further processed.
28
+ out = out.replace(/`([^`]+)`/g, (_, code: string) => `<code>${code}</code>`);
29
+ // Links: [text](url)
30
+ out = out.replace(
31
+ /\[([^\]]+)\]\(([^)]+)\)/g,
32
+ (_, text: string, url: string) =>
33
+ `<a href="${url}" class="text-ink-900 underline hover:text-ink-700 dark:text-ink-100 dark:hover:text-white" rel="noopener noreferrer">${text}</a>`,
34
+ );
35
+ // Bold: **…**
36
+ out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
37
+ // Italic: *…*
38
+ out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
39
+ return out;
40
+ }
41
+
42
+ interface Block {
43
+ type: "h2" | "h3" | "p" | "ul" | "ol" | "pre";
44
+ lines: string[];
45
+ }
46
+
47
+ function parseBlocks(md: string): Block[] {
48
+ const lines = md.split(/\r?\n/);
49
+ const blocks: Block[] = [];
50
+ let i = 0;
51
+ while (i < lines.length) {
52
+ const line = lines[i];
53
+ if (!line.trim()) {
54
+ i++;
55
+ continue;
56
+ }
57
+ if (line.startsWith("## ")) {
58
+ blocks.push({ type: "h2", lines: [line.slice(3)] });
59
+ i++;
60
+ continue;
61
+ }
62
+ if (line.startsWith("### ")) {
63
+ blocks.push({ type: "h3", lines: [line.slice(4)] });
64
+ i++;
65
+ continue;
66
+ }
67
+ if (line.startsWith("```")) {
68
+ const buf: string[] = [];
69
+ i++;
70
+ while (i < lines.length && !lines[i].startsWith("```")) {
71
+ buf.push(lines[i]);
72
+ i++;
73
+ }
74
+ i++; // skip closing fence
75
+ blocks.push({ type: "pre", lines: buf });
76
+ continue;
77
+ }
78
+ if (/^[-*] /.test(line)) {
79
+ const buf: string[] = [];
80
+ while (i < lines.length && /^[-*] /.test(lines[i])) {
81
+ buf.push(lines[i].slice(2));
82
+ i++;
83
+ }
84
+ blocks.push({ type: "ul", lines: buf });
85
+ continue;
86
+ }
87
+ if (/^\d+\. /.test(line)) {
88
+ const buf: string[] = [];
89
+ while (i < lines.length && /^\d+\. /.test(lines[i])) {
90
+ buf.push(lines[i].replace(/^\d+\. /, ""));
91
+ i++;
92
+ }
93
+ blocks.push({ type: "ol", lines: buf });
94
+ continue;
95
+ }
96
+ // Default: paragraph (consume contiguous non-blank lines).
97
+ const buf: string[] = [line];
98
+ i++;
99
+ while (
100
+ i < lines.length &&
101
+ lines[i].trim() &&
102
+ !lines[i].startsWith("## ") &&
103
+ !lines[i].startsWith("### ") &&
104
+ !lines[i].startsWith("```") &&
105
+ !/^[-*] /.test(lines[i]) &&
106
+ !/^\d+\. /.test(lines[i])
107
+ ) {
108
+ buf.push(lines[i]);
109
+ i++;
110
+ }
111
+ blocks.push({ type: "p", lines: buf });
112
+ }
113
+ return blocks;
114
+ }
115
+
116
+ export function renderMarkdown(md: string): string {
117
+ const blocks = parseBlocks(md);
118
+ const html: string[] = [];
119
+ for (const b of blocks) {
120
+ switch (b.type) {
121
+ case "h2":
122
+ html.push(
123
+ `<h2 class="m-0 mt-10 text-[1.375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h2>`,
124
+ );
125
+ break;
126
+ case "h3":
127
+ html.push(
128
+ `<h3 class="m-0 mt-8 text-[1.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h3>`,
129
+ );
130
+ break;
131
+ case "p":
132
+ html.push(
133
+ `<p class="m-0 mt-4 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines.map(renderInline).join(" ")}</p>`,
134
+ );
135
+ break;
136
+ case "ul":
137
+ html.push(
138
+ `<ul class="m-0 mt-4 list-disc space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
139
+ .map((l) => `<li>${renderInline(l)}</li>`)
140
+ .join("")}</ul>`,
141
+ );
142
+ break;
143
+ case "ol":
144
+ html.push(
145
+ `<ol class="m-0 mt-4 list-decimal space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
146
+ .map((l) => `<li>${renderInline(l)}</li>`)
147
+ .join("")}</ol>`,
148
+ );
149
+ break;
150
+ case "pre":
151
+ html.push(
152
+ `<pre 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"><code>${escapeHtml(b.lines.join("\n"))}</code></pre>`,
153
+ );
154
+ break;
155
+ }
156
+ }
157
+ return html.join("\n");
158
+ }
159
+
160
+ /** Read a markdown file from a list of candidate paths, return null if missing. */
161
+ export function readMarkdownFile(candidates: string[]): string | null {
162
+ for (const path of candidates) {
163
+ try {
164
+ if (!existsSync(path)) continue;
165
+ return readFileSync(path, "utf8");
166
+ } catch {
167
+ continue;
168
+ }
169
+ }
170
+ return null;
171
+ }