@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
@@ -1,248 +0,0 @@
1
- export type AppStatus = "active" | "quiet" | "stale" | "archived" | "unknown";
2
- export type AppLabel = "new" | "hot" | "mature" | "featured";
3
-
4
- // ── Curation: what kind of app is this? ──────────────────────────────
5
- /**
6
- * `projectType` answers "is this a real, production-grade codebase
7
- * or a demo?" — the most-asked filter for anyone scanning a list.
8
- */
9
- export type ProjectType =
10
- | "real-app" // Actually used by real people in production
11
- | "production" // Commercial product with open codebase
12
- | "reference" // Showcase of patterns/architecture, not a real product
13
- | "demo" // Demo / proof-of-concept
14
- | "template"; // Starter template others fork
15
-
16
- export type Difficulty = "beginner" | "intermediate" | "advanced";
17
- export type CodebaseSize = "small" | "medium" | "large" | "huge";
18
-
19
- // ── Scoring: 6-dimension, 0-100 each ─────────────────────────────────
20
- /**
21
- * Six orthogonal scores that drive the "decision row" UI.
22
- * - activity: how alive is the repo (commits, PRs, issues responded)
23
- * - maturity: how established (age, stability, real usage)
24
- * - learning: how good for learning (readable code, useful patterns)
25
- * - contribution: how welcoming to new contributors
26
- * - docs: how good the documentation is
27
- * - overall: composite judgment
28
- *
29
- * Scores are curator-assigned integers. They are NOT computed from
30
- * any algorithm — see `curation.notes` for the reasoning chain.
31
- */
32
- export type AppScores = {
33
- activity: number;
34
- maturity: number;
35
- learning: number;
36
- contribution: number;
37
- docs: number;
38
- overall: number;
39
- };
40
-
41
- export type GithubRepositoryMetadata = {
42
- full_name?: string;
43
- html_url?: string;
44
- homepage?: string | null;
45
- description?: string | null;
46
- fork?: boolean;
47
- archived?: boolean;
48
- disabled?: boolean;
49
- private?: boolean;
50
- visibility?: string;
51
- default_branch?: string;
52
- language?: string | null;
53
- topics?: string[];
54
- license?: {
55
- key?: string | null;
56
- name?: string | null;
57
- spdx_id?: string | null;
58
- url?: string | null;
59
- } | null;
60
- stargazers_count?: number;
61
- watchers_count?: number;
62
- forks_count?: number;
63
- open_issues_count?: number;
64
- subscribers_count?: number;
65
- size?: number;
66
- created_at?: string | null;
67
- updated_at?: string | null;
68
- pushed_at?: string | null;
69
- };
70
-
71
- export type AppHealth = {
72
- status?: string;
73
- tier?: string;
74
- visibility?: string;
75
- activityScore?: number;
76
- maintenanceScore?: number;
77
- metadataCompleteness?: number;
78
- cleanupCandidate?: boolean;
79
- staleReason?: string | null;
80
- };
81
-
82
- export type DistributionChannel = {
83
- type:
84
- | "app-store"
85
- | "play-store"
86
- | "fdroid"
87
- | "github-releases"
88
- | "apk"
89
- | "testflight"
90
- | "website"
91
- | "snapcraft"
92
- | "flathub"
93
- | "microsoft-store"
94
- | "other";
95
- platform?: string;
96
- label?: string;
97
- url: string;
98
- verified?: boolean;
99
- notes?: string;
100
- };
101
-
102
- export type AppDistribution = {
103
- channels?: DistributionChannel[];
104
- };
105
-
106
- // ── Curation provenance: was this human-reviewed? ────────────────────
107
- /**
108
- * Every curated field needs a chain of trust. `reviewed: true` means
109
- * a human looked at it and signed off. `reviewed: false` means it's
110
- * auto-generated (e.g. by AI from public repo metadata) and pending
111
- * curator review. The UI surfaces this distinction.
112
- */
113
- export type CurationInfo = {
114
- reviewed: boolean;
115
- /** Curator handle, e.g. "@username" or "SITECONFIG_NAME_PLACEHOLDER editors". */
116
- by?: string;
117
- /** ISO date the curation was authored or last reviewed. */
118
- date?: string;
119
- /** Free-text: where did the data come from? what's uncertain? */
120
- notes?: string;
121
- };
122
-
123
- // ── The app record ──────────────────────────────────────────────────
124
- /**
125
- * One open-source app entry.
126
- *
127
- * Fields split into two layers:
128
- *
129
- * 1. **Required discovery data** — name, description, repo, stack,
130
- * platforms, category. Without these, the app can't be listed.
131
- *
132
- * 2. **Optional curation data** — everything under the `// ── Curation`
133
- * block. New apps start sparse. The 5 anchor apps are fully curated.
134
- * The list/detail UI gracefully degrades when fields are missing.
135
- */
136
- export type OpenSourceApp = {
137
- // ── Identity ──
138
- slug: string;
139
- name: string;
140
- description: string;
141
- repoUrl: string;
142
- homepageUrl?: string;
143
-
144
- // ── Basic taxonomy ──
145
- /** Primary stack, e.g. "Flutter" */
146
- stack: string;
147
- /** Additional stacks used in the codebase. */
148
- stacks?: string[];
149
- platforms: string[];
150
- distribution?: AppDistribution;
151
- category: string;
152
- tags?: string[];
153
-
154
- // ── Public signals ──
155
- /** Direct URL to a square logo. Falls back to GitHub owner avatar. */
156
- logoUrl?: string;
157
- stars?: number;
158
- license?: string;
159
- status?: AppStatus;
160
- addedAt?: string;
161
- lastCommitAt?: string;
162
- labels?: AppLabel[];
163
-
164
- // ── Curation: project shape ──
165
- /** What kind of app is this? (real-app / production / reference / …) */
166
- projectType?: ProjectType;
167
- /** What state-management pattern does it use? "Riverpod", "BLoC", etc. */
168
- stateManagement?: string;
169
- /** What backend / data layer? "REST", "Firebase", "PostgreSQL", … */
170
- backend?: string;
171
- /** Architecture style. "Feature-based", "Clean", "MVC", "Layered", … */
172
- architecture?: string;
173
- /** How hard is it to read / contribute? */
174
- difficulty?: Difficulty;
175
- /** Approximate size, by curator judgment. */
176
- codebaseSize?: CodebaseSize;
177
-
178
- // ── Curation: text signals ──
179
- /**
180
- * 2-5 short phrases: "Studying offline-first architecture",
181
- * "REST API integration patterns", etc. Hand-written.
182
- */
183
- bestFor?: string[];
184
- /**
185
- * Why is this app listed? Curator's one-liner per angle.
186
- * E.g. ["Most starred Flutter app", "Real production deployment"].
187
- */
188
- whyListed?: string[];
189
- /**
190
- * Caveats / risks. Honest downsides, not marketing.
191
- * E.g. ["Large codebase", "Sparse documentation", "Archived"].
192
- */
193
- caveats?: string[];
194
-
195
- // ── Curation: contribution links ──
196
- /**
197
- * `true` if the repo has a "good first issue" label,
198
- * or a direct URL to a filtered list of them.
199
- */
200
- goodFirstIssues?: boolean | string;
201
- /** `true` if the repo has a CONTRIBUTING.md, or a direct URL. */
202
- contributionGuide?: boolean | string;
203
-
204
- // ── Curation: launch (Product-Hunt-style, optional) ──
205
- /**
206
- * For recently-launched apps, the username of the person who
207
- * submitted the launch entry. Long-running apps leave this empty.
208
- */
209
- launchedBy?: string;
210
- /**
211
- * What the maintainer wants from this launch:
212
- * "Feedback", "Contributors", "Users", "Stars", etc.
213
- */
214
- launchAsk?: string[];
215
-
216
- /**
217
- * Curated lenses — the curated "tabs" on the list page. These are
218
- * hand-assigned per app by a curator; not derived from scores.
219
- * Valid values: "good-to-learn" | "production-like" |
220
- * "beginner-friendly" | "contribution-ready" | "launches".
221
- * The label-based lenses (new / hot / mature) live in `labels`.
222
- */
223
- lenses?: string[];
224
-
225
- // ── Curation: scores ──
226
- scores?: AppScores;
227
-
228
- // ── Curation: provenance ──
229
- curation?: CurationInfo;
230
-
231
- // ── Final schema raw blocks ────────────────────────────────────────
232
- github?: {
233
- repository?: GithubRepositoryMetadata;
234
- languages?: Record<string, number>;
235
- latestRelease?: Record<string, unknown> | null;
236
- activity?: {
237
- monthlyCommits?: Array<number | { month: string; commits: number }>;
238
- totalCommitsKnown?: number;
239
- contributorsKnown?: number;
240
- openPullRequests?: number;
241
- };
242
- files?: Record<string, boolean>;
243
- labels?: Array<Record<string, unknown>>;
244
- sync?: Record<string, unknown>;
245
- };
246
- health?: AppHealth;
247
- tier?: string;
248
- };
@@ -1,228 +0,0 @@
1
- /**
2
- * Filter / sort / pagination helpers for the apps list page.
3
- *
4
- * This file is a thin proxy over `@grove-dev/ui` and `@grove-dev/core`
5
- * — the actual business logic lives there. The proxy keeps the
6
- * `OpenSourceApp` shape (defined in `data/types.ts`) compatible
7
- * with the core `AppRecord` shape.
8
- */
9
- import type { OpenSourceApp } from "../data/types";
10
-
11
- export const PAGE_SIZE = 20;
12
-
13
- export const SORT_OPTIONS = [
14
- { value: "recently-updated", label: "Recently updated" },
15
- { value: "most-starred", label: "Most starred" },
16
- { value: "recently-added", label: "Recently added" },
17
- { value: "most-mature", label: "Most mature" },
18
- { value: "alphabetical", label: "Alphabetical" },
19
- ] as const;
20
-
21
- export type AppsSort = (typeof SORT_OPTIONS)[number]["value"];
22
-
23
- export type AppsFilters = {
24
- q?: string;
25
- stacks?: string[];
26
- platforms?: string[];
27
- categories?: string[];
28
- labels?: string[];
29
- licenses?: string[];
30
- statuses?: string[];
31
- lens?: string;
32
- sort?: AppsSort;
33
- page?: number;
34
- density?: "comfortable" | "compact";
35
- };
36
-
37
- const KEYS = {
38
- q: "q",
39
- stacks: "stack",
40
- platforms: "platform",
41
- categories: "category",
42
- labels: "label",
43
- licenses: "license",
44
- statuses: "status",
45
- lens: "lens",
46
- sort: "sort",
47
- page: "page",
48
- density: "density",
49
- } as const;
50
-
51
- const DEFAULT_SORT: AppsSort = "recently-updated";
52
- const DEFAULT_PAGE = 1;
53
-
54
- export function filtersFromSearchParams(sp: URLSearchParams): AppsFilters {
55
- const rawSort = sp.get(KEYS.sort);
56
- const sort: AppsSort | undefined =
57
- rawSort && SORT_OPTIONS.some((o) => o.value === rawSort) ? (rawSort as AppsSort) : undefined;
58
- const rawPage = Number(sp.get(KEYS.page));
59
- const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : undefined;
60
- const rawDensity = sp.get(KEYS.density);
61
- const density = rawDensity === "compact" || rawDensity === "comfortable" ? rawDensity : undefined;
62
- return {
63
- q: sp.get(KEYS.q) ?? undefined,
64
- stacks: sp.getAll(KEYS.stacks).filter(Boolean),
65
- platforms: sp.getAll(KEYS.platforms).filter(Boolean),
66
- categories: sp.getAll(KEYS.categories).filter(Boolean),
67
- labels: sp.getAll(KEYS.labels).filter(Boolean),
68
- licenses: sp.getAll(KEYS.licenses).filter(Boolean),
69
- statuses: sp.getAll(KEYS.statuses).flatMap((s) => s.split(",")).filter(Boolean),
70
- lens: sp.get(KEYS.lens) ?? undefined,
71
- sort,
72
- page,
73
- density,
74
- };
75
- }
76
-
77
- export function searchParamsFromFilters(f: AppsFilters): URLSearchParams {
78
- const sp = new URLSearchParams();
79
- if (f.q) sp.set(KEYS.q, f.q);
80
- for (const v of f.stacks ?? []) sp.append(KEYS.stacks, v);
81
- for (const v of f.platforms ?? []) sp.append(KEYS.platforms, v);
82
- for (const v of f.categories ?? []) sp.append(KEYS.categories, v);
83
- for (const v of f.labels ?? []) sp.append(KEYS.labels, v);
84
- for (const v of f.licenses ?? []) sp.append(KEYS.licenses, v);
85
- if (f.statuses?.length) sp.set(KEYS.statuses, f.statuses.join(","));
86
- if (f.lens) sp.set(KEYS.lens, f.lens);
87
- if (f.sort && f.sort !== DEFAULT_SORT) sp.set(KEYS.sort, f.sort);
88
- if (f.page && f.page !== DEFAULT_PAGE) sp.set(KEYS.page, String(f.page));
89
- if (f.density && f.density !== "comfortable") sp.set(KEYS.density, f.density);
90
- return sp;
91
- }
92
-
93
- export function hasAnyFilter(f: AppsFilters): boolean {
94
- if (f.q && f.q.trim()) return true;
95
- if (f.lens) return true;
96
- return Boolean(
97
- (f.stacks?.length ?? 0) +
98
- (f.platforms?.length ?? 0) +
99
- (f.categories?.length ?? 0) +
100
- (f.labels?.length ?? 0) +
101
- (f.licenses?.length ?? 0) +
102
- (f.statuses?.length ?? 0),
103
- );
104
- }
105
-
106
- export function applySort(apps: OpenSourceApp[], sort: AppsSort): OpenSourceApp[] {
107
- const arr = apps.slice();
108
- const ts = (s?: string | null): number => (s ? new Date(s).valueOf() : 0);
109
- switch (sort) {
110
- case "most-starred":
111
- arr.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
112
- break;
113
- case "recently-updated":
114
- arr.sort((a, b) => ts(b.lastCommitAt) - ts(a.lastCommitAt));
115
- break;
116
- case "recently-added":
117
- arr.sort((a, b) => ts(b.addedAt) - ts(a.addedAt));
118
- break;
119
- case "most-mature":
120
- arr.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
121
- break;
122
- case "alphabetical":
123
- arr.sort((a, b) => a.name.localeCompare(b.name));
124
- break;
125
- }
126
- return arr;
127
- }
128
-
129
- export function paginate<T>(items: T[], page: number, pageSize = PAGE_SIZE): T[] {
130
- const start = (page - 1) * pageSize;
131
- return items.slice(start, start + pageSize);
132
- }
133
-
134
- export function totalPages(itemCount: number, pageSize = PAGE_SIZE): number {
135
- return Math.max(1, Math.ceil(itemCount / pageSize));
136
- }
137
-
138
- export function effectiveSort(f: AppsFilters): AppsSort {
139
- return f.sort ?? DEFAULT_SORT;
140
- }
141
- export function effectivePage(f: AppsFilters): number {
142
- return f.page ?? DEFAULT_PAGE;
143
- }
144
- export function effectiveDensity(f: AppsFilters): "comfortable" | "compact" {
145
- return f.density ?? "comfortable";
146
- }
147
-
148
- export function filterApps(apps: OpenSourceApp[], f: AppsFilters): OpenSourceApp[] {
149
- const q = f.q?.trim().toLowerCase();
150
- const stacks = f.stacks?.length ? new Set(f.stacks) : null;
151
- const platforms = f.platforms?.length ? new Set(f.platforms) : null;
152
- const categories = f.categories?.length ? new Set(f.categories) : null;
153
- const labels = f.labels?.length ? new Set(f.labels) : null;
154
- const licenses = f.licenses?.length ? new Set(f.licenses) : null;
155
- const statuses = f.statuses?.length ? new Set(f.statuses) : null;
156
-
157
- return apps.filter((a) => {
158
- if (q) {
159
- const ownerMatch = /github\.com\/([^/]+)\//.exec(a.repoUrl)?.[1]?.toLowerCase() ?? "";
160
- const haystack = [a.name, ownerMatch, a.description, a.category].join(" ").toLowerCase();
161
- if (!haystack.includes(q)) return false;
162
- }
163
- if (stacks) {
164
- const allStacks = [a.stack, ...(a.stacks ?? [])];
165
- if (!allStacks.some((s) => stacks.has(s))) return false;
166
- }
167
- if (platforms) {
168
- if (!a.platforms.some((p) => platforms.has(p))) return false;
169
- }
170
- if (categories) {
171
- if (!categories.has(a.category)) return false;
172
- }
173
- if (labels) {
174
- if (!a.labels?.some((l) => labels.has(l))) return false;
175
- }
176
- if (licenses) {
177
- if (!a.license || !licenses.has(a.license)) return false;
178
- }
179
- if (statuses) {
180
- if (!a.status || !statuses.has(a.status)) return false;
181
- }
182
- if (f.lens) {
183
- if (!a.lenses?.includes(f.lens)) return false;
184
- }
185
- return true;
186
- });
187
- }
188
-
189
- export function activeFilterChips(
190
- f: AppsFilters,
191
- ): { key: keyof AppsFilters; value: string; label: string }[] {
192
- const out: { key: keyof AppsFilters; value: string; label: string }[] = [];
193
- if (f.q && f.q.trim()) out.push({ key: "q", value: "", label: `“${f.q}”` });
194
- for (const v of f.stacks ?? []) out.push({ key: "stacks", value: v, label: `stack: ${v}` });
195
- for (const v of f.platforms ?? []) out.push({ key: "platforms", value: v, label: `platform: ${v}` });
196
- for (const v of f.categories ?? []) out.push({ key: "categories", value: v, label: `category: ${v}` });
197
- for (const v of f.labels ?? []) out.push({ key: "labels", value: v, label: v });
198
- for (const v of f.licenses ?? []) out.push({ key: "licenses", value: v, label: `license: ${v}` });
199
- if (f.lens) out.push({ key: "lens", value: f.lens, label: `lens: ${f.lens}` });
200
- return out;
201
- }
202
-
203
- export function buildFacets(apps: OpenSourceApp[]) {
204
- const counts = {
205
- stack: new Map<string, number>(),
206
- platform: new Map<string, number>(),
207
- category: new Map<string, number>(),
208
- label: new Map<string, number>(),
209
- license: new Map<string, number>(),
210
- };
211
- for (const a of apps) {
212
- const allStacks = new Set([a.stack, ...(a.stacks ?? [])]);
213
- for (const s of allStacks) counts.stack.set(s, (counts.stack.get(s) ?? 0) + 1);
214
- for (const p of a.platforms) counts.platform.set(p, (counts.platform.get(p) ?? 0) + 1);
215
- counts.category.set(a.category, (counts.category.get(a.category) ?? 0) + 1);
216
- for (const l of a.labels ?? []) counts.label.set(l, (counts.label.get(l) ?? 0) + 1);
217
- if (a.license) counts.license.set(a.license, (counts.license.get(a.license) ?? 0) + 1);
218
- }
219
- const sortByCountThenName = (m: Map<string, number>) =>
220
- [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([value, count]) => ({ value, count }));
221
- return {
222
- stacks: sortByCountThenName(counts.stack),
223
- platforms: sortByCountThenName(counts.platform),
224
- categories: sortByCountThenName(counts.category),
225
- labels: sortByCountThenName(counts.label),
226
- licenses: sortByCountThenName(counts.license),
227
- };
228
- }
@@ -1,36 +0,0 @@
1
- /** Number/date formatting helpers used across the templates. */
2
- export function compact(n: number): string {
3
- if (n >= 1000) {
4
- const k = n / 1000;
5
- return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
6
- }
7
- return n.toString();
8
- }
9
-
10
- export function formatStars(n: number | null | undefined): string | null {
11
- if (typeof n !== "number" || !Number.isFinite(n)) return null;
12
- return compact(n);
13
- }
14
-
15
- export function formatRelative(iso: string | null | undefined): string {
16
- if (!iso) return "—";
17
- const d = new Date(iso);
18
- if (Number.isNaN(d.valueOf())) return "—";
19
- const days = Math.floor((Date.now() - d.valueOf()) / (1000 * 60 * 60 * 24));
20
- if (days <= 0) return "today";
21
- if (days === 1) return "1d ago";
22
- if (days < 30) return `${days}d ago`;
23
- const months = Math.floor(days / 30);
24
- if (months === 1) return "1mo ago";
25
- if (months < 12) return `${months}mo ago`;
26
- const years = Math.floor(months / 12);
27
- if (years === 1) return "1y ago";
28
- return `${years}y ago`;
29
- }
30
-
31
- export function formatDate(iso: string | null | undefined): string {
32
- if (!iso) return "—";
33
- const d = new Date(iso);
34
- if (Number.isNaN(d.valueOf())) return iso;
35
- return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
36
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * Curated lenses. The proxy lives here so the page can stay clean.
3
- * For a generic, headless version, see `@grove-dev/ui`.
4
- */
5
-
6
- export type LensId =
7
- | "all"
8
- | "new"
9
- | "hot"
10
- | "mature"
11
- | "good-to-learn"
12
- | "production-like"
13
- | "beginner-friendly"
14
- | "contribution-ready"
15
- | "launches"
16
- | "actively-developed"
17
- | "needs-maintainer";
18
-
19
- export interface LensDef {
20
- id: LensId;
21
- label: string;
22
- description?: string;
23
- toParams: () => Record<string, string | string[]>;
24
- }
25
-
26
- export const LENSES: LensDef[] = [
27
- { id: "all", label: "All apps", description: "Every app in the directory", toParams: () => ({}) },
28
- { id: "new", label: "Newly added", description: "Recently added to the directory", toParams: () => ({ label: "new" }) },
29
- { id: "hot", label: "Hot right now", description: "Apps gaining attention or recent activity", toParams: () => ({ label: "hot" }) },
30
- { id: "mature", label: "Mature", description: "Established long-running projects", toParams: () => ({ label: "mature" }) },
31
- { id: "production-like", label: "Production-grade", description: "Real apps, not toy projects", toParams: () => ({ lens: "production-like" }) },
32
- { id: "good-to-learn", label: "Good to learn", description: "Readable codebases with useful patterns", toParams: () => ({ lens: "good-to-learn" }) },
33
- { id: "beginner-friendly", label: "Beginner friendly", description: "Smaller, readable, easier to understand", toParams: () => ({ lens: "beginner-friendly" }) },
34
- { id: "contribution-ready", label: "Contribution ready", description: "Clear issues, active maintainers, license, contribution docs", toParams: () => ({ lens: "contribution-ready" }) },
35
- { id: "launches", label: "Launches", description: "Recently launched OSS apps seeking feedback", toParams: () => ({ lens: "launches" }) },
36
- { id: "actively-developed", label: "Actively developed", description: "Recent commits, releases, issue activity", toParams: () => ({ status: "active" }) },
37
- { id: "needs-maintainer", label: "Needs maintainer", description: "Useful apps that need help", toParams: () => ({ status: "stale,quiet" }) },
38
- ];
39
-
40
- export const PRIMARY_LENSES: LensId[] = [
41
- "all",
42
- "new",
43
- "hot",
44
- "mature",
45
- "production-like",
46
- "good-to-learn",
47
- ];
48
-
49
- export function lensById(id: string | null | undefined): LensDef | undefined {
50
- if (!id) return undefined;
51
- return LENSES.find((l) => l.id === id);
52
- }
53
-
54
- export function isLensActive(lensId: LensId, sp: URLSearchParams): boolean {
55
- if (lensId === "all") {
56
- return !lensFromSearchParams(sp) && !sp.get("label") && !sp.get("status");
57
- }
58
- const def = lensById(lensId);
59
- if (!def) return false;
60
- const target = def.toParams();
61
- for (const [k, v] of Object.entries(target)) {
62
- if (Array.isArray(v)) {
63
- if (sp.getAll(k).length !== v.length) return false;
64
- for (const item of v) if (!sp.getAll(k).includes(item)) return false;
65
- } else {
66
- if (sp.get(k) !== v) return false;
67
- }
68
- }
69
- return true;
70
- }
71
-
72
- export function isPrimaryLens(id: string | null | undefined): boolean {
73
- if (!id) return false;
74
- return PRIMARY_LENSES.includes(id as LensId);
75
- }
76
-
77
- export function lensToQuery(id: LensId | null | undefined, current: URLSearchParams): string {
78
- if (!id || id === "all") return "";
79
- const def = lensById(id);
80
- if (!def) return "";
81
- const next = new URLSearchParams(current);
82
- for (const [k, v] of Object.entries(def.toParams())) {
83
- if (Array.isArray(v)) {
84
- next.delete(k);
85
- for (const item of v) next.append(k, item);
86
- } else {
87
- next.set(k, v);
88
- }
89
- }
90
- return next.toString();
91
- }
92
-
93
- export function lensFromSearchParams(sp: URLSearchParams): LensId | null {
94
- const v = sp.get("lens");
95
- if (!v) return null;
96
- if (LENSES.some((l) => l.id === v)) return v as LensId;
97
- return null;
98
- }
@@ -1,12 +0,0 @@
1
- /** GitHub repo URL parsing helpers. */
2
- export function getOwnerAndRepoFromRepoUrl(repoUrl: string): { owner: string | null; repo: string | null } {
3
- if (!repoUrl || !repoUrl.includes("github.com/")) return { owner: null, repo: null };
4
- const m = repoUrl.match(/github\.com\/([^/]+)\/([^/?#]+)/);
5
- if (!m) return { owner: null, repo: null };
6
- return { owner: m[1], repo: m[2].replace(/\.git$/, "") };
7
- }
8
-
9
- export function getOwnerAvatarUrl(owner: string | null, size: number = 80): string | null {
10
- if (!owner) return null;
11
- return `https://avatars.githubusercontent.com/${owner}?v=4&s=${size}`;
12
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Score display helpers used across list and detail pages.
3
- * Generic version lives in `@grove-dev/ui` — this proxies the
4
- * `AppScores` shape defined in template `data/types.ts`.
5
- */
6
- import type { AppScores } from "../data/types";
7
-
8
- export function scoreTier(n: number): 0 | 1 | 2 | 3 | 4 {
9
- if (n < 20) return 0;
10
- if (n < 40) return 1;
11
- if (n < 60) return 2;
12
- if (n < 80) return 3;
13
- return 4;
14
- }
15
-
16
- export function scoreTierLabel(n: number): string {
17
- switch (scoreTier(n)) {
18
- case 0: return "Very low";
19
- case 1: return "Low";
20
- case 2: return "Medium";
21
- case 3: return "High";
22
- case 4: return "Very high";
23
- }
24
- }
25
-
26
- export function scoreLabel(n: number | undefined): string {
27
- if (typeof n !== "number") return "—";
28
- return Math.round(n).toString();
29
- }
30
-
31
- export const SCORE_DIMENSIONS: (keyof AppScores)[] = [
32
- "activity", "maturity", "learning", "contribution", "docs", "overall",
33
- ];
34
-
35
- export const SCORE_LABELS: Record<keyof AppScores, string> = {
36
- activity: "Activity",
37
- maturity: "Maturity",
38
- learning: "Learning",
39
- contribution: "Contribution",
40
- docs: "Docs",
41
- overall: "Overall",
42
- };
43
-
44
- export const SCORE_REASONING: Record<keyof AppScores, string> = {
45
- activity: "Recent commits, open and merged PRs, issue response time, release cadence.",
46
- maturity: "Repo age, stable structure, real-world production usage, contributor stability.",
47
- learning: "Codebase readability, useful architecture patterns, comment quality, examples.",
48
- contribution: "Open issues, recent merged PRs from new contributors, maintainer responsiveness, contribution guide.",
49
- docs: "README quality, architecture docs, contributing guide, examples, code comments.",
50
- overall: "Composite judgment — how likely this app is to satisfy a curious developer.",
51
- };