@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,159 @@
1
+ ---
2
+ /**
3
+ * Header — sticky brand + nav + theme toggle.
4
+ *
5
+ * Generic: takes a `site` prop with brand + nav. No `siteConfig`
6
+ * import — consumers compose the props from their own config.
7
+ *
8
+ * Optional `submit` button renders a "Submit app" / "Submit project"
9
+ * call-to-action on the right side of the nav. Set `submitHref` /
10
+ * `submitLabel` to enable, leave them undefined for a clean header.
11
+ *
12
+ * Optional `repoUrl` adds a "View source" GitHub button. Set
13
+ * `repoLabel` to change the visible text.
14
+ */
15
+
16
+ import ThemeToggle from "./ThemeToggle.astro";
17
+
18
+ interface NavItem {
19
+ label: string;
20
+ href: string;
21
+ }
22
+
23
+ interface Site {
24
+ name: string;
25
+ tagline?: string;
26
+ nav: NavItem[];
27
+ description?: string;
28
+ repoUrl?: string;
29
+ }
30
+
31
+ interface Props {
32
+ site: Site;
33
+ /** Submit button label, e.g. "Submit app". Omit to hide. */
34
+ submitLabel?: string;
35
+ /** Submit button href, e.g. "/submit". Omit to hide. */
36
+ submitHref?: string;
37
+ /** GitHub button label, e.g. "View source". */
38
+ repoLabel?: string;
39
+ /** Extra class for the outer header element. */
40
+ class?: string;
41
+ }
42
+
43
+ const {
44
+ site,
45
+ submitLabel,
46
+ submitHref,
47
+ repoLabel = "Star",
48
+ class: className = "",
49
+ } = Astro.props;
50
+ ---
51
+
52
+ <header
53
+ class:list={[
54
+ "sticky top-0 z-50 border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85",
55
+ className,
56
+ ]}
57
+ >
58
+ <div
59
+ class="mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
60
+ style="max-width: var(--container-wide, 1400px);"
61
+ >
62
+ <a
63
+ href="/"
64
+ class="inline-flex items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
65
+ >
66
+ <span
67
+ aria-hidden="true"
68
+ class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
69
+ ></span>
70
+ <span>{site.name}</span>
71
+ </a>
72
+
73
+ <nav class="hidden flex-1 md:block" aria-label="Primary">
74
+ <ul class="m-0 flex list-none gap-5 p-0">
75
+ {site.nav.map((item) => (
76
+ <li>
77
+ <a
78
+ href={item.href}
79
+ class="text-[0.875rem] font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
80
+ >
81
+ {item.label}
82
+ </a>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ </nav>
87
+
88
+ <div class="ml-auto flex items-center gap-2">
89
+ {submitHref && (
90
+ <a
91
+ href={submitHref}
92
+ class="grove-btn grove-btn-outline"
93
+ >
94
+ <svg
95
+ viewBox="0 0 16 16"
96
+ width="13"
97
+ height="13"
98
+ aria-hidden="true"
99
+ fill="currentColor"
100
+ >
101
+ <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"></path>
102
+ </svg>
103
+ <span>{submitLabel ?? "Submit"}</span>
104
+ </a>
105
+ )}
106
+
107
+ {site.repoUrl && (
108
+ <a
109
+ href={site.repoUrl}
110
+ target="_blank"
111
+ rel="noopener noreferrer"
112
+ aria-label={repoLabel}
113
+ title={repoLabel}
114
+ class="grove-btn grove-btn-outline"
115
+ >
116
+ <svg
117
+ viewBox="0 0 16 16"
118
+ width="16"
119
+ height="16"
120
+ aria-hidden="true"
121
+ fill="currentColor"
122
+ >
123
+ <path
124
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"
125
+ ></path>
126
+ </svg>
127
+ <span>{repoLabel}</span>
128
+ </a>
129
+ )}
130
+
131
+ <ThemeToggle />
132
+ </div>
133
+ </div>
134
+ </header>
135
+
136
+ <script>
137
+ // Smooth-scroll for in-page anchor links (Header nav + Hero CTA).
138
+ // Works on any <a href="#…"> that points to an existing element on
139
+ // the current page. External links (e.g. /about, /projects?…) are
140
+ // skipped because they don't start with "#". Modifier-clicks
141
+ // (Cmd/Ctrl/Shift/middle) let the browser handle the anchor
142
+ // normally so users can open the same section in a new tab.
143
+ document.addEventListener("click", (e) => {
144
+ if (e.defaultPrevented) return;
145
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
146
+ if (e.button !== 0) return;
147
+ const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
148
+ 'a[href^="#"]',
149
+ );
150
+ if (!link) return;
151
+ const id = link.getAttribute("href")!;
152
+ if (id.length <= 1) return;
153
+ const target = document.querySelector(id);
154
+ if (!target) return;
155
+ e.preventDefault();
156
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
157
+ history.replaceState(null, "", id);
158
+ });
159
+ </script>
@@ -0,0 +1,58 @@
1
+ ---
2
+ /**
3
+ * SectionHeader — eyebrow + title + description, used to title
4
+ * every section on the home page. Optionally centered (good for
5
+ * "Why this exists" and friends).
6
+ */
7
+
8
+ interface Props {
9
+ eyebrow?: string;
10
+ title: string;
11
+ description?: string;
12
+ align?: "left" | "center";
13
+ /** 2 (default) for top-level sections, 3 for nested ones. */
14
+ level?: 2 | 3;
15
+ /** Extra class for the outer header element. */
16
+ class?: string;
17
+ }
18
+
19
+ const {
20
+ eyebrow,
21
+ title,
22
+ description,
23
+ align = "left",
24
+ level = 2,
25
+ class: className = "",
26
+ } = Astro.props;
27
+ const Heading = `h${level}` as "h2" | "h3";
28
+ ---
29
+
30
+ <header
31
+ class:list={[
32
+ "mb-6 flex flex-col gap-2",
33
+ align === "center" ? "items-center text-center" : "",
34
+ className,
35
+ ]}
36
+ >
37
+ {eyebrow && (
38
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
39
+ {eyebrow}
40
+ </span>
41
+ )}
42
+ <div class="flex flex-wrap items-end justify-between gap-3">
43
+ <Heading class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
44
+ {title}
45
+ </Heading>
46
+ <slot name="actions" />
47
+ </div>
48
+ {description && (
49
+ <p
50
+ class:list={[
51
+ "m-0 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400",
52
+ align === "center" ? "max-w-2xl" : "max-w-[56ch]",
53
+ ]}
54
+ >
55
+ {description}
56
+ </p>
57
+ )}
58
+ </header>
@@ -1,36 +1,44 @@
1
1
  ---
2
- import { siteConfig } from "../../data/config";
3
- // SPDX-License-Identifier: MIT
4
-
5
2
  /**
6
- * Per-page SEO: <title>, description, canonical, Open Graph, Twitter Card,
7
- * and JSON-LD structured data. Designed to be used inside <head> by the
8
- * BaseLayout. Pages pass the rich props they care about; everything else
9
- * has a sensible default derived from the site config and the Astro URL.
3
+ * Seo — per-page <head> content: title, description, canonical,
4
+ * Open Graph, Twitter Card, and JSON-LD structured data. Designed
5
+ * to be used inside the <head> slot of BaseLayout. Pages pass the
6
+ * rich props they care about; everything else has a sensible default
7
+ * derived from `site` and the Astro URL.
10
8
  *
11
9
  * Renders:
12
10
  * - <title>, meta description, canonical link
13
11
  * - Open Graph (og:*) for Facebook, LinkedIn, Discord, Slack, etc.
14
12
  * - Twitter Card (twitter:*) for X / Twitter
15
13
  * - JSON-LD WebSite + Organization on every page
16
- * - JSON-LD SoftwareSourceCode (or custom) when `jsonLd` is provided
17
- * (e.g. on /apps/[slug] for the per-app structured data)
14
+ * - JSON-LD custom payload (e.g. SoftwareSourceCode) when `jsonLd`
15
+ * is provided.
18
16
  *
19
- * Noindex is opt-in via the `noindex` prop. By default everything is
20
- * indexable; the only noindex target today is the thin /submit wrapper.
17
+ * Noindex is opt-in via the `noindex` prop. By default everything
18
+ * is indexable.
21
19
  */
22
20
 
21
+ interface SiteRef {
22
+ name: string;
23
+ description?: string;
24
+ url?: string;
25
+ repoUrl?: string;
26
+ searchPath?: string;
27
+ }
28
+
23
29
  interface Props {
24
30
  title: string;
25
31
  description: string;
26
- /** Open Graph / Twitter image. Default: /og-image.svg. Should be absolute or root-relative. */
32
+ /** Open Graph / Twitter image. Default: /og-image.svg. */
27
33
  image?: string;
28
- /** og:type. Default: "website". Use "article" for blog posts / changelog. */
34
+ /** og:type. Default: "website". */
29
35
  type?: "website" | "article" | "profile";
30
36
  /** Set true to add noindex,nofollow. */
31
37
  noindex?: boolean;
32
- /** Extra JSON-LD payload (object) — typically SoftwareSourceCode on app pages. */
38
+ /** Extra JSON-LD payload (object) — typically SoftwareSourceCode. */
33
39
  jsonLd?: Record<string, unknown> | Record<string, unknown>[];
40
+ /** Site identity (name, description, url, repoUrl). Required. */
41
+ site: SiteRef;
34
42
  }
35
43
 
36
44
  const {
@@ -40,45 +48,53 @@ const {
40
48
  type = "website",
41
49
  noindex = false,
42
50
  jsonLd,
51
+ site,
43
52
  } = Astro.props;
44
53
 
45
- const site = Astro.site ?? new URL("https://open-apps.dev");
46
- const canonical = new URL(Astro.url.pathname, site).toString();
47
- const ogImage = new URL(image, site).toString();
54
+ const siteUrl = site.url
55
+ ? new URL(site.url)
56
+ : (Astro.site ?? new URL("https://grove.dev"));
57
+ const canonical = new URL(Astro.url.pathname, siteUrl).toString();
58
+ const ogImage = new URL(image, siteUrl).toString();
48
59
 
49
- // Truncate description for OG/Twitter where longer is worse (160–200 chars).
60
+ // Truncate description for OG/Twitter where longer is worse (160-200 chars).
50
61
  function truncate(s: string, max: number): string {
51
62
  if (s.length <= max) return s;
52
63
  return s.slice(0, max - 1).trimEnd() + "\u2026";
53
64
  }
54
65
  const shortDescription = truncate(description, 200);
66
+ const siteDescription =
67
+ site.description ??
68
+ "A growing community knowledge site powered by Grove.";
55
69
 
56
- // WebSite + Organization JSON-LD. These are site-wide identity signals
57
- // that should appear on every page (Google uses them for site links
58
- // and the knowledge panel).
59
- const websiteLd = {
70
+ // JSON-LD: site-wide identity. Google uses these for site links
71
+ // and the knowledge panel.
72
+ const searchPath = site.searchPath ?? "/projects";
73
+ const websiteLd: Record<string, unknown> = {
60
74
  "@context": "https://schema.org",
61
75
  "@type": "WebSite",
62
- name: `${siteConfig.name}`,
63
- description:
64
- "A curated, self-refreshing directory of real open-source application codebases.",
65
- url: site.toString().replace(/\/$/, ""),
76
+ name: site.name,
77
+ description: siteDescription,
78
+ url: siteUrl.toString().replace(/\/$/, ""),
66
79
  inLanguage: "en",
67
80
  potentialAction: {
68
81
  "@type": "SearchAction",
69
- target: `${site.toString().replace(/\/$/, "")}/apps?q={search_term_string}`,
82
+ target: `${siteUrl.toString().replace(/\/$/, "")}${searchPath}?q={search_term_string}`,
70
83
  "query-input": "required name=search_term_string",
71
84
  },
72
85
  };
73
86
 
74
- const organizationLd = {
87
+ const organizationLd: Record<string, unknown> = {
75
88
  "@context": "https://schema.org",
76
89
  "@type": "Organization",
77
- name: `${siteConfig.name}`,
78
- url: site.toString().replace(/\/$/, ""),
79
- logo: new URL("/og-image.svg", site).toString(),
80
- sameAs: ["https://github.com/tortuvshin/open-apps"],
90
+ name: site.name,
91
+ url: siteUrl.toString().replace(/\/$/, ""),
92
+ logo: new URL("/og-image.svg", siteUrl).toString(),
81
93
  };
94
+
95
+ if (site.repoUrl) {
96
+ organizationLd.sameAs = [site.repoUrl];
97
+ }
82
98
  ---
83
99
 
84
100
  <!-- Primary meta -->
@@ -95,8 +111,8 @@ const organizationLd = {
95
111
  <meta property="og:image" content={ogImage} />
96
112
  <meta property="og:image:width" content="1200" />
97
113
  <meta property="og:image:height" content="630" />
98
- <meta property="og:image:alt" content="{siteConfig.name} — a directory of real open-source apps" />
99
- <meta property="og:site_name" content="{siteConfig.name}" />
114
+ <meta property="og:image:alt" content={`${site.name} — ${siteDescription}`} />
115
+ <meta property="og:site_name" content={site.name} />
100
116
  <meta property="og:locale" content="en" />
101
117
 
102
118
  <!-- Twitter Card -->
@@ -104,13 +120,21 @@ const organizationLd = {
104
120
  <meta name="twitter:title" content={title} />
105
121
  <meta name="twitter:description" content={shortDescription} />
106
122
  <meta name="twitter:image" content={ogImage} />
107
- <meta name="twitter:image:alt" content="{siteConfig.name} — a directory of real open-source apps" />
123
+ <meta name="twitter:image:alt" content={`${site.name} — ${siteDescription}`} />
108
124
 
109
125
  <!-- JSON-LD: site-wide identity -->
110
- <script is:inline type="application/ld+json" set:html={JSON.stringify(websiteLd)} />
111
- <script is:inline type="application/ld+json" set:html={JSON.stringify(organizationLd)} />
126
+ <script
127
+ is:inline
128
+ type="application/ld+json"
129
+ set:html={JSON.stringify(websiteLd)}
130
+ ></script>
131
+ <script
132
+ is:inline
133
+ type="application/ld+json"
134
+ set:html={JSON.stringify(organizationLd)}
135
+ ></script>
112
136
 
113
- <!-- JSON-LD: per-page (SoftwareSourceCode for /apps/[slug], etc.) -->
137
+ <!-- JSON-LD: per-page (SoftwareSourceCode for [slug], etc.) -->
114
138
  {jsonLd && (
115
139
  <script
116
140
  is:inline
@@ -0,0 +1,110 @@
1
+ ---
2
+ /**
3
+ * ThemeToggle — a three-position theme switcher (light / dark / system).
4
+ *
5
+ * State is persisted in `localStorage` under the key "grove-theme"
6
+ * (matching `STORAGE_KEY` in `BaseLayout.astro`'s THEME_INIT script).
7
+ *
8
+ * The actual `<html class="dark">` toggle happens in the inline
9
+ * THEME_INIT script in BaseLayout — that script runs before paint
10
+ * so the user never sees a flash of the wrong theme.
11
+ *
12
+ * This button just rotates the stored mode and dispatches a
13
+ * `theme:set` event that the THEME_INIT listener picks up.
14
+ */
15
+
16
+ interface Props {
17
+ class?: string;
18
+ }
19
+
20
+ const { class: className = "" } = Astro.props;
21
+ ---
22
+
23
+ <button
24
+ type="button"
25
+ data-theme-toggle
26
+ aria-label="Toggle theme"
27
+ title="Toggle theme"
28
+ class:list={[
29
+ "relative inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200 dark:hover:border-ink-700 dark:hover:bg-ink-900/40",
30
+ className,
31
+ ]}
32
+ >
33
+ {/* Sun — shown in dark mode (click to go light) */}
34
+ <svg
35
+ class="hidden h-4 w-4 dark:block"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ stroke-width="1.8"
40
+ stroke-linecap="round"
41
+ stroke-linejoin="round"
42
+ aria-hidden="true"
43
+ >
44
+ <circle cx="12" cy="12" r="4"></circle>
45
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>
46
+ </svg>
47
+ {/* Moon — shown in light mode (click to go dark) */}
48
+ <svg
49
+ class="block h-4 w-4 dark:hidden"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="1.8"
54
+ stroke-linecap="round"
55
+ stroke-linejoin="round"
56
+ aria-hidden="true"
57
+ >
58
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"></path>
59
+ </svg>
60
+ </button>
61
+
62
+ <script>
63
+ // The toggle button simply rotates between the three modes on
64
+ // click. The actual class application lives in the page-level
65
+ // THEME_INIT script so behaviour stays consistent.
66
+ const STORAGE_KEY = "grove-theme";
67
+ type Mode = "light" | "dark" | "system";
68
+
69
+ function currentMode(): Mode {
70
+ try {
71
+ const stored = localStorage.getItem(STORAGE_KEY);
72
+ return stored === "light" || stored === "dark" || stored === "system"
73
+ ? stored
74
+ : "system";
75
+ } catch {
76
+ return "system";
77
+ }
78
+ }
79
+
80
+ function rotate(): Mode {
81
+ const order: Mode[] = ["light", "dark", "system"];
82
+ const cur = currentMode();
83
+ const next = order[(order.indexOf(cur) + 1) % order.length];
84
+ try {
85
+ localStorage.setItem(STORAGE_KEY, next);
86
+ } catch {
87
+ // localStorage may be blocked (private mode, etc.) — we still
88
+ // dispatch the event so the UI updates for the current page.
89
+ }
90
+ // Notify the page-level listener (THEME_INIT).
91
+ window.dispatchEvent(new CustomEvent("theme:set", { detail: next }));
92
+ return next;
93
+ }
94
+
95
+ function updateLabel(btn: HTMLButtonElement) {
96
+ btn.title = `Theme: ${currentMode()} (click to switch)`;
97
+ }
98
+
99
+ document
100
+ .querySelectorAll<HTMLButtonElement>("[data-theme-toggle]")
101
+ .forEach((btn) => {
102
+ updateLabel(btn);
103
+ btn.addEventListener("click", () => {
104
+ rotate();
105
+ updateLabel(btn);
106
+ });
107
+ btn.addEventListener("mouseenter", () => updateLabel(btn));
108
+ btn.addEventListener("focus", () => updateLabel(btn));
109
+ });
110
+ </script>
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Type declarations for the .astro layout components.
3
+ *
4
+ * The `.astro` files are excluded from `tsc` (the build only emits
5
+ * TypeScript helpers, not compiled components), so we declare the
6
+ * module shape here for the rare consumer that wants to write:
7
+ *
8
+ * import { BaseLayout } from "@grove-dev/astro/layouts";
9
+ *
10
+ * Consumers that just want the components should import them by
11
+ * file:
12
+ *
13
+ * import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
14
+ *
15
+ * The `AstroComponentFactory` is the runtime type Astro uses for
16
+ * every `.astro` module's default export. In a normal Astro project
17
+ * this is declared by the generated `.astro/types.d.ts` (produced
18
+ * by `astro sync`); the `@grove-dev/astro` package itself doesn't
19
+ * run an Astro build, so we re-declare it here.
20
+ */
21
+
22
+ declare module "*.astro" {
23
+ // Pull the factory type from Astro's server runtime. The path
24
+ // `astro/runtime/server` is a documented deep import that has
25
+ // shipped across Astro 4 and 5.
26
+ import type { AstroComponentFactory } from "astro/runtime/server";
27
+ const component: AstroComponentFactory;
28
+ export default component;
29
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @grove-dev/astro — generic layout components.
3
+ *
4
+ * These layouts are the public, framework-agnostic shell used by
5
+ * every consumer page. They:
6
+ *
7
+ * - take a `site` prop (name, tagline, nav, repoUrl) instead of
8
+ * importing a `siteConfig` from a project-local path;
9
+ * - compose the standard Header / Footer / Seo / Container /
10
+ * SectionHeader / ThemeToggle components;
11
+ * - share the `grove-theme` localStorage key for theme state.
12
+ *
13
+ * Re-exported as ESM module names. Astro resolves the
14
+ * `@grove-dev/astro/layouts/*` subpath import from `src/layouts/`
15
+ * (the package's `dist/` only contains TypeScript helpers, not the
16
+ * `.astro` files themselves).
17
+ */
18
+ export { default as BaseLayout } from "./BaseLayout.astro";
19
+ export { default as Container } from "./Container.astro";
20
+ export { default as Footer } from "./Footer.astro";
21
+ export { default as Header } from "./Header.astro";
22
+ export { default as SectionHeader } from "./SectionHeader.astro";
23
+ export { default as Seo } from "./Seo.astro";
24
+ export { default as ThemeToggle } from "./ThemeToggle.astro";
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Display-name helpers for curation labels, statuses, lenses, and
3
+ * sort options. Internal ids (e.g. "hot", "stale", "production-like")
4
+ * stay stable for URL state and data files; this module owns the
5
+ * mapping from id → user-facing string.
6
+ *
7
+ * Every export is dependency-free so it can be imported by both
8
+ * server-rendered Astro components and any client-side script.
9
+ *
10
+ * Status ids accept the `HealthStatus` union from `@grove-dev/core`
11
+ * plus a couple of curated composites (e.g. "needs-maintainer")
12
+ * that appear in URL state.
13
+ */
14
+
15
+ import type {
16
+ AppLabel,
17
+ HealthStatus,
18
+ } from "@grove-dev/core";
19
+
20
+ // ── Label ids (the `curation.labels` array on a record) ──────────
21
+ export type LabelId = AppLabel;
22
+
23
+ export const LABEL_DISPLAY: Record<LabelId, string> = {
24
+ new: "Recently added",
25
+ hot: "Trending",
26
+ mature: "Established",
27
+ featured: "Featured",
28
+ };
29
+
30
+ /** Pretty-print a slug to Title Case. Used for arbitrary slugs. */
31
+ export function prettySlug(slug: string | null | undefined): string {
32
+ if (!slug) return "";
33
+ return slug
34
+ .replace(/[-_]+/g, " ")
35
+ .replace(/\s+/g, " ")
36
+ .trim()
37
+ .split(" ")
38
+ .map((w) => (w ? w[0]!.toUpperCase() + w.slice(1) : ""))
39
+ .join(" ");
40
+ }
41
+
42
+ /** Returns a human-readable name for a label id, or null if unknown. */
43
+ export function labelDisplay(id: string | null | undefined): string | null {
44
+ if (!id) return null;
45
+ if (id in LABEL_DISPLAY) {
46
+ return LABEL_DISPLAY[id as LabelId];
47
+ }
48
+ return prettySlug(id);
49
+ }
50
+
51
+ // ── Status ids (the `health.status` field on a record) ───────────
52
+ /**
53
+ * Canonical status display map. Includes the curated
54
+ * "needs-maintainer" composite that's emitted by the
55
+ * `needs-maintainer` lens.
56
+ */
57
+ export const STATUS_DISPLAY: Record<string, string> = {
58
+ active: "Active",
59
+ mature: "Mature",
60
+ stale: "Not recently active",
61
+ inactive: "Inactive",
62
+ archived: "Archived",
63
+ unknown: "Unknown",
64
+ historical: "Historical",
65
+ needs_review: "Needs review",
66
+ quiet: "Quiet",
67
+ unavailable: "Unavailable",
68
+ "needs-maintainer": "Needs maintainer",
69
+ };
70
+
71
+ export function statusDisplay(
72
+ s: HealthStatus | string | null | undefined,
73
+ ): string {
74
+ if (!s) return STATUS_DISPLAY.unknown;
75
+ return STATUS_DISPLAY[s] ?? prettySlug(s);
76
+ }
77
+
78
+ // ── Lens ids (the curated tabs on /projects) ─────────────────────
79
+ export const LENS_DISPLAY: Record<string, string> = {
80
+ all: "All",
81
+ hot: "Trending",
82
+ new: "Recently added",
83
+ mature: "Established",
84
+ featured: "Featured",
85
+ "needs-review": "Needs review",
86
+ };
87
+
88
+ export function lensDisplay(id: string | null | undefined): string {
89
+ if (!id) return LENS_DISPLAY.all;
90
+ return LENS_DISPLAY[id] ?? prettySlug(id);
91
+ }
92
+
93
+ // ── Sort ids (the /projects sort dropdown) ───────────────────────
94
+ export const SORT_DISPLAY: Record<string, string> = {
95
+ "recently-updated": "Recently updated",
96
+ "most-starred": "Most starred",
97
+ "recently-added": "Recently added",
98
+ "best-overall": "Best overall",
99
+ alphabetical: "Alphabetical",
100
+ };
101
+
102
+ export function sortDisplay(id: string | null | undefined): string {
103
+ if (!id) return SORT_DISPLAY["recently-updated"];
104
+ return SORT_DISPLAY[id] ?? prettySlug(id);
105
+ }
106
+
107
+ // ── Status option list (used by filter dropdowns) ───────────────
108
+ // "needs-maintainer" is included even though it's a derived/composite
109
+ // status — filter dropdowns and chip rendering need to offer it
110
+ // alongside the raw HealthStatus values.
111
+ export const STATUS_OPTIONS: string[] = [
112
+ "active",
113
+ "quiet",
114
+ "stale",
115
+ "needs-maintainer",
116
+ "archived",
117
+ "unknown",
118
+ ];