@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
@@ -0,0 +1,197 @@
1
+ ---
2
+ /**
3
+ * RefinePanel — multi-select facet dropdowns.
4
+ *
5
+ * Renders a row of facet dropdowns (Stack, Platform, Category,
6
+ * License) plus a Sort dropdown, all using native <select> for
7
+ * server-render-only behavior. Selection auto-submits the form
8
+ * to the same list page with the chosen values merged into the URL.
9
+ *
10
+ * The form is a GET to the list page base, with each facet as a
11
+ * repeated key. Empty values are dropped, so the URL stays clean.
12
+ *
13
+ * Generic: takes a `facets` map of `{ value, label, count }[]` per
14
+ * dimension and an `initial` `AppsFilters` for preselecting the
15
+ * current URL state.
16
+ */
17
+
18
+ import type { AppsFilters } from "../lib/search";
19
+ import { SORT_OPTIONS } from "../lib/search";
20
+
21
+ interface FacetValue {
22
+ value: string;
23
+ label?: string;
24
+ count?: number;
25
+ }
26
+
27
+ interface Facets {
28
+ stacks?: FacetValue[];
29
+ platforms?: FacetValue[];
30
+ categories?: FacetValue[];
31
+ labels?: FacetValue[];
32
+ licenses?: FacetValue[];
33
+ }
34
+
35
+ interface Props {
36
+ facets: Facets;
37
+ initial: AppsFilters;
38
+ pathPrefix?: string;
39
+ class?: string;
40
+ /** Hide specific facets. Default: all shown. */
41
+ hide?: Array<"stack" | "platform" | "category" | "license" | "sort">;
42
+ }
43
+
44
+ const {
45
+ facets,
46
+ initial,
47
+ pathPrefix = "/",
48
+ class: className = "",
49
+ hide = [],
50
+ } = Astro.props;
51
+
52
+ const baseAction = pathPrefix.replace(/\/$/, "") || "/";
53
+
54
+ const initialStacks = new Set(initial.stacks ?? []);
55
+ const initialPlatforms = new Set(initial.platforms ?? []);
56
+ const initialCategories = new Set(initial.categories ?? []);
57
+ const initialLicenses = new Set(initial.licenses ?? []);
58
+ const initialSort = initial.sort ?? "";
59
+ ---
60
+
61
+ <form
62
+ method="get"
63
+ action={baseAction}
64
+ class:list={[
65
+ "grove-refine-panel flex flex-wrap items-end gap-3",
66
+ className,
67
+ ]}
68
+ data-grove-refine-panel
69
+ >
70
+ {initial.q && <input type="hidden" name="q" value={initial.q} />}
71
+ {initial.lens && <input type="hidden" name="lens" value={initial.lens} />}
72
+
73
+ {!hide.includes("stack") && (facets.stacks?.length ?? 0) > 0 && (
74
+ <label class="flex flex-col gap-1">
75
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
76
+ Stack
77
+ </span>
78
+ <select
79
+ name="stack"
80
+ multiple
81
+ size="1"
82
+ onchange="this.form?.requestSubmit()"
83
+ class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
84
+ >
85
+ {facets.stacks!.map((f) => (
86
+ <option
87
+ value={f.value}
88
+ selected={initialStacks.has(f.value)}
89
+ >
90
+ {f.label ?? f.value}
91
+ {typeof f.count === "number" ? ` (${f.count})` : ""}
92
+ </option>
93
+ ))}
94
+ </select>
95
+ </label>
96
+ )}
97
+
98
+ {!hide.includes("platform") && (facets.platforms?.length ?? 0) > 0 && (
99
+ <label class="flex flex-col gap-1">
100
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
101
+ Platform
102
+ </span>
103
+ <select
104
+ name="platform"
105
+ multiple
106
+ size="1"
107
+ onchange="this.form?.requestSubmit()"
108
+ class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
109
+ >
110
+ {facets.platforms!.map((f) => (
111
+ <option
112
+ value={f.value}
113
+ selected={initialPlatforms.has(f.value)}
114
+ >
115
+ {f.label ?? f.value}
116
+ {typeof f.count === "number" ? ` (${f.count})` : ""}
117
+ </option>
118
+ ))}
119
+ </select>
120
+ </label>
121
+ )}
122
+
123
+ {!hide.includes("category") && (facets.categories?.length ?? 0) > 0 && (
124
+ <label class="flex flex-col gap-1">
125
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
126
+ Category
127
+ </span>
128
+ <select
129
+ name="category"
130
+ multiple
131
+ size="1"
132
+ onchange="this.form?.requestSubmit()"
133
+ class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
134
+ >
135
+ {facets.categories!.map((f) => (
136
+ <option
137
+ value={f.value}
138
+ selected={initialCategories.has(f.value)}
139
+ >
140
+ {f.label ?? f.value}
141
+ {typeof f.count === "number" ? ` (${f.count})` : ""}
142
+ </option>
143
+ ))}
144
+ </select>
145
+ </label>
146
+ )}
147
+
148
+ {!hide.includes("license") && (facets.licenses?.length ?? 0) > 0 && (
149
+ <label class="flex flex-col gap-1">
150
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
151
+ License
152
+ </span>
153
+ <select
154
+ name="license"
155
+ multiple
156
+ size="1"
157
+ onchange="this.form?.requestSubmit()"
158
+ class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
159
+ >
160
+ {facets.licenses!.map((f) => (
161
+ <option
162
+ value={f.value}
163
+ selected={initialLicenses.has(f.value)}
164
+ >
165
+ {f.label ?? f.value}
166
+ {typeof f.count === "number" ? ` (${f.count})` : ""}
167
+ </option>
168
+ ))}
169
+ </select>
170
+ </label>
171
+ )}
172
+
173
+ {!hide.includes("sort") && (
174
+ <label class="flex flex-col gap-1">
175
+ <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
176
+ Sort
177
+ </span>
178
+ <select
179
+ name="sort"
180
+ onchange="this.form?.requestSubmit()"
181
+ class="grove-select min-w-[12rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
182
+ >
183
+ {SORT_OPTIONS.map((o) => (
184
+ <option value={o.value} selected={initialSort === o.value}>
185
+ {o.label}
186
+ </option>
187
+ ))}
188
+ </select>
189
+ </label>
190
+ )}
191
+
192
+ <noscript>
193
+ <button type="submit" class="grove-btn grove-btn-outline">
194
+ Apply
195
+ </button>
196
+ </noscript>
197
+ </form>
@@ -1,32 +1,102 @@
1
1
  ---
2
+ /**
3
+ * ScoreBars — 4-bar score visualization.
4
+ *
5
+ * Renders a horizontal 4-cell bar per score dimension. Each cell
6
+ * fills based on the score tier (0..4) computed from a 0-100 value.
7
+ * The 5 dimensions are: activity, maturity, learning, contribution,
8
+ * docs. The "overall" bar is optional and rendered last as a
9
+ * composite when `showOverall` is true.
10
+ *
11
+ * Renders nothing when the record has no score block.
12
+ */
13
+
2
14
  import type { Score } from "@grove-dev/core";
3
- import { SCORE_LABELS, scoreTier } from "@grove-dev/ui";
15
+ import { scoreTier, scoreTierLabel, SCORE_LABELS } from "../lib/scores";
4
16
 
5
17
  interface Props {
6
- scores?: Score;
18
+ scores: Score | undefined | null;
19
+ /** Show the "overall" composite bar. Default true. */
7
20
  showOverall?: boolean;
21
+ /** Compact mode — no labels next to the bars. */
8
22
  compact?: boolean;
23
+ class?: string;
24
+ }
25
+
26
+ const {
27
+ scores,
28
+ showOverall = true,
29
+ compact = false,
30
+ class: className = "",
31
+ } = Astro.props;
32
+
33
+ const dimensions: (keyof Score)[] = showOverall
34
+ ? ["activity", "maturity", "learning", "contribution", "docs", "overall"]
35
+ : ["activity", "maturity", "learning", "contribution", "docs"];
36
+
37
+ function dim(n: number | undefined): number {
38
+ if (typeof n !== "number") return 0;
39
+ return scoreTier(n);
9
40
  }
10
41
 
11
- const { scores = {}, showOverall = true, compact = false } = Astro.props;
12
- const dimensions = (["activity", "maturity", "learning", "contribution", "docs", "overall"] as const)
13
- .filter((dimension) => showOverall || dimension !== "overall");
14
- const hasAny = Object.values(scores).some((value) => typeof value === "number");
42
+ function label(n: number | undefined): string {
43
+ if (typeof n !== "number") return "";
44
+ return Math.round(n).toString();
45
+ }
15
46
  ---
16
47
 
17
- <dl class="oc-scores" aria-label="Curation scores">
18
- {!hasAny && <span class="oc-muted">Scores not yet curated</span>}
19
- {hasAny && dimensions.map((dimension) => {
20
- const value = scores[dimension];
21
- const tier = scoreTier(value);
22
- return (
23
- <div>
24
- <dt>{SCORE_LABELS[dimension]}</dt>
25
- {!compact && <dd class="oc-score-value">{typeof value === "number" ? Math.round(value) : "-"}</dd>}
26
- <dd class="oc-score-bar" aria-label={`${SCORE_LABELS[dimension]} ${value ?? 0} of 100`}>
27
- {[0, 1, 2, 3].map((index) => <span class={index < tier ? "is-filled" : ""}></span>)}
28
- </dd>
29
- </div>
30
- );
31
- })}
32
- </dl>
48
+ {scores && (
49
+ <div
50
+ class:list={["grove-score-bars flex flex-col gap-2", className]}
51
+ aria-label="Project scores"
52
+ >
53
+ {dimensions.map((d) => {
54
+ const n = scores[d];
55
+ const tier = dim(n);
56
+ return (
57
+ <div
58
+ class:list={[
59
+ "grove-score-row flex items-center gap-2",
60
+ compact ? "text-2xs" : "text-2xs",
61
+ ]}
62
+ title={`${SCORE_LABELS[d]}: ${label(n)} (${scoreTierLabel(typeof n === "number" ? n : 0)})`}
63
+ >
64
+ {!compact && (
65
+ <span class="w-20 flex-shrink-0 font-medium text-ink-700 dark:text-ink-300">
66
+ {SCORE_LABELS[d]}
67
+ </span>
68
+ )}
69
+ <span
70
+ class:list={[
71
+ "grove-score-cells flex gap-0.5",
72
+ compact ? "w-20" : "w-24",
73
+ ]}
74
+ role="img"
75
+ aria-label={`${SCORE_LABELS[d]}: ${label(n)}`}
76
+ >
77
+ {[0, 1, 2, 3, 4].map((i) => (
78
+ <span
79
+ class:list={[
80
+ "grove-score-cell h-1.5 flex-1 rounded-sm",
81
+ i < tier
82
+ ? d === "overall"
83
+ ? "bg-ink-900 dark:bg-ink-100"
84
+ : "bg-ink-700 dark:bg-ink-300"
85
+ : "bg-ink-100 dark:bg-ink-900",
86
+ ]}
87
+ ></span>
88
+ ))}
89
+ </span>
90
+ <span
91
+ class:list={[
92
+ "font-mono text-ink-500 dark:text-ink-400",
93
+ compact ? "ml-1" : "ml-2 w-7 text-right",
94
+ ]}
95
+ >
96
+ {label(n)}
97
+ </span>
98
+ </div>
99
+ );
100
+ })}
101
+ </div>
102
+ )}
@@ -0,0 +1,71 @@
1
+ ---
2
+ /**
3
+ * SmartLensTabs — curated single-select lens tabs.
4
+ *
5
+ * Renders the primary lenses (All / Trending / Recently added /
6
+ * Established / Featured / Needs review) as a horizontal tab bar.
7
+ * Clicking a tab navigates to the same list page with the `lens=`
8
+ * query param updated. Other filter params (q, stack, platform,
9
+ * category, sort) are preserved.
10
+ *
11
+ * The current selection is computed from `currentParams`, so the
12
+ * component is server-render-only — no client JS required.
13
+ *
14
+ * Generic: uses the `LENSES` registry from `lib/lenses.ts` and
15
+ * takes the list-page base via `pathPrefix`.
16
+ */
17
+
18
+ import { LENSES, hrefForLens, isLensActive } from "../lib/lenses";
19
+ import { lensDisplay } from "../lib/display";
20
+
21
+ interface Props {
22
+ currentParams: URLSearchParams;
23
+ pathPrefix?: string;
24
+ class?: string;
25
+ /** Override the visible lens list (defaults to LENSES). */
26
+ lensIds?: string[];
27
+ }
28
+
29
+ const {
30
+ currentParams,
31
+ pathPrefix = "/",
32
+ class: className = "",
33
+ lensIds,
34
+ } = Astro.props;
35
+
36
+ // If `lensIds` is provided, filter LENSES to those ids in that order.
37
+ const lenses =
38
+ lensIds && lensIds.length > 0
39
+ ? lensIds
40
+ .map((id) => LENSES.find((l) => l.id === id))
41
+ .filter((l): l is NonNullable<typeof l> => Boolean(l))
42
+ : LENSES;
43
+ ---
44
+
45
+ <nav
46
+ class:list={[
47
+ "grove-lens-tabs flex flex-wrap items-center gap-1.5",
48
+ className,
49
+ ]}
50
+ aria-label="Curated views"
51
+ >
52
+ {lenses.map((lens) => {
53
+ const active = isLensActive(lens.id, currentParams);
54
+ return (
55
+ <a
56
+ href={hrefForLens(lens.id, currentParams, pathPrefix)}
57
+ class:list={[
58
+ "grove-btn rounded-md px-3 py-1.5 text-2xs font-medium transition-colors",
59
+ active
60
+ ? "grove-btn-primary"
61
+ : "grove-btn-ghost border border-ink-200 dark:border-ink-800",
62
+ ]}
63
+ aria-current={active ? "page" : undefined}
64
+ data-event="lens_tab_click"
65
+ data-event-source="smart_lens_tabs"
66
+ >
67
+ {lensDisplay(lens.id)}
68
+ </a>
69
+ );
70
+ })}
71
+ </nav>
@@ -0,0 +1,132 @@
1
+ ---
2
+ /**
3
+ * StackGrid — browse-by-stack grid for the home page.
4
+ *
5
+ * Renders a card per stack with name, blurb, status pill
6
+ * (live/expanding/planned), and a count. The href is the list
7
+ * page filtered to that stack, so consumers compose the URL via
8
+ * the `hrefFor` prop.
9
+ *
10
+ * Generic: no blueprint-specific terms. A "stack" here is just a
11
+ * slug + display name + status; consumers derive both from their
12
+ * own data.
13
+ */
14
+
15
+ interface StackEntry {
16
+ name: string;
17
+ slug: string;
18
+ blurb?: string;
19
+ status?: "live" | "expanding" | "planned";
20
+ count?: number;
21
+ }
22
+
23
+ interface Props {
24
+ stacks: StackEntry[];
25
+ /** List page base. */
26
+ pathPrefix?: string;
27
+ /** Stack icon registry — maps slug → icon file name. Optional. */
28
+ iconAliases?: Record<string, string>;
29
+ /** Extra class. */
30
+ class?: string;
31
+ /** Hide the section eyebrow / header entirely. */
32
+ bare?: boolean;
33
+ }
34
+
35
+ const {
36
+ stacks,
37
+ pathPrefix = "/",
38
+ iconAliases,
39
+ class: className = "",
40
+ bare = false,
41
+ } = Astro.props;
42
+
43
+ function buildHref(slug: string): string {
44
+ const base = pathPrefix.replace(/\/$/, "");
45
+ return `${base}?stack=${encodeURIComponent(slug)}`;
46
+ }
47
+
48
+ function statusLabel(s: StackEntry["status"]): string | null {
49
+ switch (s) {
50
+ case "live":
51
+ return "Live";
52
+ case "expanding":
53
+ return "Expanding";
54
+ case "planned":
55
+ return "Planned";
56
+ default:
57
+ return null;
58
+ }
59
+ }
60
+ ---
61
+
62
+ <section
63
+ class:list={["grove-section", className]}
64
+ aria-labelledby={bare ? undefined : "grove-stackgrid-heading"}
65
+ >
66
+ <div class="grove-container-wide mx-auto px-5 sm:px-7">
67
+ {!bare && (
68
+ <header class="mb-6 flex flex-col gap-2">
69
+ <h2
70
+ id="grove-stackgrid-heading"
71
+ class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
72
+ >
73
+ Browse by stack
74
+ </h2>
75
+ <p class="m-0 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
76
+ Every record is tagged with the technologies it depends on. Pick a
77
+ stack to see what's shipping in that world.
78
+ </p>
79
+ </header>
80
+ )}
81
+
82
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
83
+ {stacks.map((s) => (
84
+ <a
85
+ href={buildHref(s.slug)}
86
+ class="grove-card grove-card-hover group flex flex-col gap-1.5 p-4 no-underline"
87
+ data-event="stack_grid_click"
88
+ data-event-source="stack_grid"
89
+ >
90
+ <div class="flex items-baseline justify-between gap-2">
91
+ <h3 class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
92
+ {s.name}
93
+ </h3>
94
+ {typeof s.count === "number" && (
95
+ <span class="font-mono text-2xs text-ink-500 dark:text-ink-400">
96
+ {s.count}
97
+ </span>
98
+ )}
99
+ </div>
100
+ {s.blurb && (
101
+ <p class="m-0 line-clamp-2 text-[0.8125rem] leading-snug text-ink-500 dark:text-ink-400">
102
+ {s.blurb}
103
+ </p>
104
+ )}
105
+ {statusLabel(s.status) && (
106
+ <span
107
+ class:list={[
108
+ "grove-badge mt-1 self-start",
109
+ s.status === "live"
110
+ ? "grove-badge-new"
111
+ : s.status === "expanding"
112
+ ? "grove-badge-mature"
113
+ : "grove-badge-stale",
114
+ ]}
115
+ >
116
+ {statusLabel(s.status)}
117
+ </span>
118
+ )}
119
+ </a>
120
+ ))}
121
+ </div>
122
+ </div>
123
+ </section>
124
+
125
+ <style>
126
+ .line-clamp-2 {
127
+ display: -webkit-box;
128
+ -webkit-line-clamp: 2;
129
+ -webkit-box-orient: vertical;
130
+ overflow: hidden;
131
+ }
132
+ </style>
@@ -0,0 +1,116 @@
1
+ ---
2
+ /**
3
+ * WhyThisExists — the "what is this site for?" section.
4
+ *
5
+ * Renders 3 short, opinionated points (icon + title + body). The
6
+ * icon set is small and generic: "app" (window/grid glyph),
7
+ * "code" (angle brackets), "search" (magnifier). Defaults to
8
+ * the canonical "directory, source, search" trio.
9
+ *
10
+ * Generic: every point comes through props. No blueprint-specific
11
+ * copy. Consumers can pass `points` to swap the order or copy.
12
+ */
13
+
14
+ interface WhyPoint {
15
+ title: string;
16
+ body: string;
17
+ icon: "app" | "code" | "search";
18
+ }
19
+
20
+ interface Props {
21
+ points?: WhyPoint[];
22
+ title?: string;
23
+ description?: string;
24
+ class?: string;
25
+ }
26
+
27
+ const {
28
+ points,
29
+ title = "Why this directory exists",
30
+ description = "A short note on the point of all this.",
31
+ class: className = "",
32
+ } = Astro.props;
33
+
34
+ const defaultPoints: WhyPoint[] = [
35
+ {
36
+ icon: "app",
37
+ title: "A curated starting point",
38
+ body: "Skip the rabbit holes. Every entry here is something a real developer reached for, vetted for quality and longevity.",
39
+ },
40
+ {
41
+ icon: "code",
42
+ title: "Real, runnable source",
43
+ body: "Every project links back to its source. Skim, fork, learn — no paywalls, no broken download links.",
44
+ },
45
+ {
46
+ icon: "search",
47
+ title: "Search that respects you",
48
+ body: "Filter by stack, platform, license, or status. No SEO spam, no affiliate farms. The directory is the product.",
49
+ },
50
+ ];
51
+
52
+ const items = points && points.length > 0 ? points : defaultPoints;
53
+ ---
54
+
55
+ <section
56
+ class:list={["grove-section", className]}
57
+ aria-labelledby="grove-why-heading"
58
+ >
59
+ <div class="grove-container mx-auto px-5 sm:px-7">
60
+ <header class="mb-8 flex flex-col items-center gap-2 text-center">
61
+ <h2
62
+ id="grove-why-heading"
63
+ class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
64
+ >
65
+ {title}
66
+ </h2>
67
+ {description && (
68
+ <p class="m-0 max-w-2xl text-[0.9375rem] text-ink-500 dark:text-ink-400">
69
+ {description}
70
+ </p>
71
+ )}
72
+ </header>
73
+
74
+ <ol
75
+ class="m-0 grid list-none grid-cols-1 gap-6 p-0 md:grid-cols-3"
76
+ aria-label="Why this directory"
77
+ >
78
+ {items.map((p, i) => (
79
+ <li class="flex flex-col items-center gap-2 text-center">
80
+ <span
81
+ aria-hidden="true"
82
+ class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200"
83
+ >
84
+ {p.icon === "app" && (
85
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
86
+ <path d="M1.75 3.5a.25.25 0 0 1 .25-.25h12a.25.25 0 0 1 .25.25v8a.25.25 0 0 1-.25.25h-12a.25.25 0 0 1-.25-.25v-8ZM2 4.75v6h12v-6H2ZM2.5 6.5h4v2.5h-4V6.5Z"></path>
87
+ </svg>
88
+ )}
89
+ {p.icon === "code" && (
90
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
91
+ <path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25Zm6.56 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L13.94 8l-3.72-3.72a.75.75 0 0 1 0-1.06Z"></path>
92
+ </svg>
93
+ )}
94
+ {p.icon === "search" && (
95
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
96
+ <path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"></path>
97
+ </svg>
98
+ )}
99
+ </span>
100
+ <span
101
+ aria-hidden="true"
102
+ class="font-mono text-2xs text-ink-400 dark:text-ink-500"
103
+ >
104
+ 0{i + 1}
105
+ </span>
106
+ <h3 class="m-0 text-base font-semibold tracking-tight text-ink-900 dark:text-ink-100">
107
+ {p.title}
108
+ </h3>
109
+ <p class="m-0 max-w-xs text-[0.875rem] leading-relaxed text-ink-500 dark:text-ink-400">
110
+ {p.body}
111
+ </p>
112
+ </li>
113
+ ))}
114
+ </ol>
115
+ </div>
116
+ </section>