@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,669 +0,0 @@
1
- ---
2
- import { siteConfig } from "../../data/config";
3
- import BaseLayout from "../../components/layout/BaseLayout.astro";
4
- import Container from "../../components/layout/Container.astro";
5
- import SmartLensTabs from "../../components/SmartLensTabs.astro";
6
- import RefinePanel from "../../components/RefinePanel.astro";
7
- import AppsIndexRow from "../../components/AppsIndexRow.astro";
8
- import AppsPagination from "../../components/AppsPagination.astro";
9
- import ExploreByCategory from "../../components/ExploreByCategory.astro";
10
- import ExploreByStack from "../../components/ExploreByStack.astro";
11
-
12
- import { apps } from "../../data/apps";
13
- import {
14
- buildFacets,
15
- filtersFromSearchParams,
16
- searchParamsFromFilters,
17
- filterApps,
18
- activeFilterChips,
19
- applySort,
20
- paginate,
21
- totalPages,
22
- PAGE_SIZE,
23
- effectiveSort,
24
- effectivePage,
25
- effectiveDensity,
26
- SORT_OPTIONS,
27
- type AppsFilters,
28
- } from "../../lib/apps-search";
29
- import { lensFromSearchParams } from "../../lib/lenses";
30
- import { categories } from "../../data/categories";
31
- import { stacks } from "../../data/stacks";
32
- import { stats } from "../../data/stats";
33
-
34
- // Parse URL → filters, then derive the effective (post-default) values.
35
- const initialFilters = filtersFromSearchParams(Astro.url.searchParams);
36
- const initialLens = lensFromSearchParams(Astro.url.searchParams);
37
- const facets = buildFacets(apps);
38
- const sort = effectiveSort(initialFilters);
39
- const page = effectivePage(initialFilters);
40
- const density = effectiveDensity(initialFilters);
41
- const initialChips = activeFilterChips(initialFilters);
42
-
43
- // Apply filter → sort → paginate server-side. URL is the source of
44
- // truth; this page is fully SSR. No client-side render loop.
45
- const filtered = filterApps(apps, initialFilters);
46
- const sorted = applySort(filtered, sort);
47
- const totalFiltered = sorted.length;
48
- const totalFilteredPages = totalPages(totalFiltered);
49
- const safePage = Math.min(page, totalFilteredPages);
50
- const pageApps = paginate(sorted, safePage);
51
- const initialChipsTotal = initialChips.length;
52
-
53
- const githubUrl = stats.originalRepo;
54
-
55
- // Build a "next page" / "prev page" href that preserves every other
56
- // filter param. Reused by the pagination component.
57
- function hrefForPage(target: number): string {
58
- const next: AppsFilters = { ...initialFilters, page: target };
59
- const sp = searchParamsFromFilters(next);
60
- const qs = sp.toString();
61
- return qs ? `/apps?${qs}` : "/apps";
62
- }
63
-
64
- function hrefForDensity(value: "comfortable" | "compact"): string {
65
- const next: AppsFilters = { ...initialFilters, density: value, page: 1 };
66
- const sp = searchParamsFromFilters(next);
67
- const qs = sp.toString();
68
- return qs ? `/apps?${qs}` : "/apps";
69
- }
70
-
71
- function hrefForClearAll(): string {
72
- return "/apps";
73
- }
74
-
75
- // Human-readable summary line under the search.
76
- const sortLabel = SORT_OPTIONS.find((o) => o.value === sort)?.label ?? "Recently updated";
77
- const filterSummary: string[] = [];
78
- if (initialFilters.q) filterSummary.push(`matching "${initialFilters.q}"`);
79
- if (initialFilters.stacks?.length) filterSummary.push(`stack: ${initialFilters.stacks.join(", ")}`);
80
- if (initialFilters.platforms?.length)
81
- filterSummary.push(`platform: ${initialFilters.platforms.join(", ")}`);
82
- if (initialFilters.categories?.length)
83
- filterSummary.push(`category: ${initialFilters.categories.join(", ")}`);
84
- if (initialFilters.licenses?.length)
85
- filterSummary.push(`license: ${initialFilters.licenses.join(", ")}`);
86
- if (initialFilters.labels?.length) filterSummary.push(`label: ${initialFilters.labels.join(", ")}`);
87
- if (initialLens) filterSummary.push(`lens: ${initialLens}`);
88
-
89
- const title = "Browse open-source apps — ${siteConfig.name}";
90
- const description =
91
- "Search and filter real open-source applications by platform, stack, category, license, and activity.";
92
- ---
93
-
94
- <BaseLayout title={title} description={description}>
95
- {/* ── Top: title + search + sticky toolbar ── */}
96
- <section
97
- class="border-b border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950"
98
- >
99
- <Container>
100
- <div class="py-8 sm:py-10">
101
- <span
102
- class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
103
- >Apps</span
104
- >
105
- <h1
106
- class="mt-1 m-0 text-[1.75rem] sm:text-[2rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100"
107
- >
108
- Browse open-source apps
109
- </h1>
110
- <p
111
- class="mt-2 m-0 max-w-[64ch] text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400"
112
- >
113
- Search and filter real open-source applications by platform,
114
- stack, category, license, and activity. Every entry is a real
115
- codebase with hand-curated context.
116
- </p>
117
-
118
- {/* Sticky toolbar. Holds: search, results summary, sort,
119
- density toggle. Once it sticks on scroll, the chips
120
- layer + refine button follow along below. */}
121
- <div
122
- id="apps-toolbar"
123
- class="sticky top-[57px] z-30 mt-6 -mx-5 border-y border-ink-200 bg-white/95 px-5 backdrop-blur supports-[backdrop-filter]:bg-white/80 dark:border-ink-800 dark:bg-ink-950/95 dark:supports-[backdrop-filter]:bg-ink-950/80 sm:-mx-7 sm:px-7"
124
- >
125
- <form
126
- id="search-form"
127
- class="flex flex-col gap-3 py-3 sm:flex-row sm:items-center"
128
- role="search"
129
- >
130
- <label class="relative flex-1">
131
- <span class="sr-only">Search apps by name, description, owner, category</span>
132
- <svg
133
- viewBox="0 0 16 16"
134
- width="16"
135
- height="16"
136
- aria-hidden="true"
137
- class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
138
- fill="currentColor"
139
- >
140
- <path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z" />
141
- </svg>
142
- <input
143
- id="search-input"
144
- type="search"
145
- name="q"
146
- value={initialFilters.q ?? ""}
147
- placeholder="Search by app name, category, stack, platform, or license..."
148
- autocomplete="off"
149
- class="w-full rounded-md border border-ink-200 bg-white py-2 pl-9 pr-3 text-[0.875rem] text-ink-900 placeholder:text-ink-400 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 dark:placeholder:text-ink-500 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
150
- />
151
- </label>
152
-
153
- {/* Sort dropdown */}
154
- <label class="relative">
155
- <span class="sr-only">Sort apps</span>
156
- <select
157
- id="sort-select"
158
- class="h-9 cursor-pointer appearance-none rounded-md border border-ink-200 bg-white py-1.5 pl-3 pr-8 text-[0.8125rem] font-medium text-ink-700 transition-colors hover:border-ink-300 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-300 dark:hover:border-ink-700 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
159
- >
160
- {SORT_OPTIONS.map((o) => (
161
- <option value={o.value} selected={o.value === sort}>{o.label}</option>
162
- ))}
163
- </select>
164
- <svg
165
- viewBox="0 0 16 16"
166
- width="11"
167
- height="11"
168
- aria-hidden="true"
169
- class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-400"
170
- fill="currentColor"
171
- >
172
- <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"/>
173
- </svg>
174
- </label>
175
-
176
- {/* Density toggle: comfortable / compact */}
177
- <div
178
- class="inline-flex h-9 items-center rounded-md border border-ink-200 bg-white p-0.5 dark:border-ink-800 dark:bg-ink-950"
179
- role="group"
180
- aria-label="List density"
181
- >
182
- <button
183
- type="button"
184
- data-density-toggle="comfortable"
185
- data-href={hrefForDensity("comfortable")}
186
- class={`inline-flex h-7 items-center rounded-[5px] px-2 text-[0.75rem] font-medium transition-colors ${
187
- density === "comfortable"
188
- ? "bg-ink-900 text-white dark:bg-ink-100 dark:text-ink-900"
189
- : "text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
190
- }`}
191
- aria-pressed={density === "comfortable" ? "true" : "false"}
192
- >
193
- Comfortable
194
- </button>
195
- <button
196
- type="button"
197
- data-density-toggle="compact"
198
- data-href={hrefForDensity("compact")}
199
- class={`inline-flex h-7 items-center rounded-[5px] px-2 text-[0.75rem] font-medium transition-colors ${
200
- density === "compact"
201
- ? "bg-ink-900 text-white dark:bg-ink-100 dark:text-ink-900"
202
- : "text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
203
- }`}
204
- aria-pressed={density === "compact" ? "true" : "false"}
205
- >
206
- Compact
207
- </button>
208
- </div>
209
- </form>
210
-
211
- {/* Result count summary + active filter chips row. */}
212
- <div
213
- id="result-summary"
214
- class="flex flex-col gap-2 border-t border-ink-100 py-2.5 text-2xs dark:border-ink-900 sm:flex-row sm:items-center sm:justify-between"
215
- >
216
- <p class="m-0 text-ink-500 dark:text-ink-400">
217
- <span
218
- id="apps-results-count"
219
- class="font-mono text-ink-900 dark:text-ink-100"
220
- data-total={totalFiltered}
221
- data-page-count={pageApps.length}
222
- data-page-size={PAGE_SIZE}
223
- data-page={safePage}
224
- data-pages={totalFilteredPages}
225
- >
226
- {totalFiltered} {totalFiltered === 1 ? "app" : "apps"} found
227
- </span>
228
- {filterSummary.length > 0 && (
229
- <span id="apps-filters-summary" class="text-ink-400 dark:text-ink-500">
230
- {" · "}filtered by {filterSummary.join(" · ")}
231
- </span>
232
- )}
233
- <span class="text-ink-400 dark:text-ink-500">
234
- {" · "}sorted by {sortLabel}
235
- </span>
236
- {totalFilteredPages > 1 && (
237
- <span class="text-ink-400 dark:text-ink-500">
238
- {" · "}page {safePage} of {totalFilteredPages}
239
- </span>
240
- )}
241
- </p>
242
-
243
- {initialChipsTotal > 0 && (
244
- <div
245
- id="apps-active-chips"
246
- class="flex flex-wrap items-center gap-1.5"
247
- aria-label="Active filters"
248
- >
249
- {initialChips.map((chip) => (
250
- <button
251
- type="button"
252
- class="apps-active-chip group inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
253
- data-remove-key={chip.key}
254
- data-remove-value={chip.value}
255
- aria-label={`Remove filter ${chip.label}`}
256
- >
257
- <span>{chip.label}</span>
258
- <span aria-hidden="true" class="text-ink-400 group-hover:text-ink-700 dark:text-ink-500 dark:group-hover:text-ink-200">×</span>
259
- </button>
260
- ))}
261
- <a
262
- id="apps-clear-all"
263
- href={hrefForClearAll()}
264
- class="text-2xs text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
265
- >
266
- Clear all
267
- </a>
268
- </div>
269
- )}
270
- </div>
271
- </div>
272
- </div>
273
- </Container>
274
- </section>
275
-
276
- {/* ── Lenses + Filter bar — single toolbar that combines
277
- "what lens am I in" (curated views) with "what fields
278
- are filtered" (drop-down filters). Stays compact. ── */}
279
- <section class="border-b border-ink-200 dark:border-ink-800">
280
- <Container>
281
- <div class="flex flex-col gap-3 py-4">
282
- <div class="flex flex-wrap items-center gap-x-3 gap-y-2">
283
- <span
284
- class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400"
285
- >Lenses</span
286
- >
287
- <SmartLensTabs currentParams={Astro.url.searchParams} />
288
- </div>
289
- <div class="flex flex-wrap items-center gap-x-3 gap-y-2">
290
- <span
291
- class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400"
292
- >Filters</span
293
- >
294
- <RefinePanel
295
- facets={facets}
296
- initial={initialFilters}
297
- />
298
- </div>
299
- </div>
300
- </Container>
301
- </section>
302
-
303
- {/* ── Results ── */}
304
- <section class="border-b border-ink-200 dark:border-ink-800">
305
- <Container>
306
- <div class="py-8 sm:py-10">
307
- {pageApps.length === 0 ? (
308
- <div
309
- id="empty-state"
310
- class="flex flex-col items-center gap-2 py-16 text-center"
311
- >
312
- <p class="m-0 text-[0.9375rem] font-medium text-ink-700 dark:text-ink-200">
313
- No apps match these filters.
314
- </p>
315
- <p class="m-0 max-w-[40ch] text-2xs text-ink-500 dark:text-ink-400">
316
- Try removing a filter, clearing the search, or picking a
317
- different lens.
318
- </p>
319
- <a href={hrefForClearAll()} class="mt-3 btn-outline">Clear all filters</a>
320
- </div>
321
- ) : (
322
- <div
323
- id="apps-results-list"
324
- data-density={density}
325
- class={`flex flex-col gap-3 ${density === "compact" ? "apps-density-compact" : ""}`}
326
- >
327
- {pageApps.map((app) => <AppsIndexRow app={app} />)}
328
- </div>
329
- )}
330
-
331
- <AppsPagination
332
- current={safePage}
333
- total={totalFilteredPages}
334
- hrefFor={hrefForPage}
335
- />
336
- </div>
337
- </Container>
338
- </section>
339
-
340
- {/* ── Explore: navigation (not filters) ── */}
341
- <ExploreByCategory categories={categories} />
342
- <ExploreByStack stacks={stacks} />
343
-
344
- {/* ── Submit CTA ── */}
345
- <section class="border-t border-ink-200 dark:border-ink-800">
346
- <Container>
347
- <div class="flex flex-col items-center gap-3 py-12 text-center">
348
- <h2
349
- class="m-0 text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
350
- >
351
- Know a real open-source app that belongs here?
352
- </h2>
353
- <p
354
- class="m-0 max-w-[48ch] text-[0.875rem] text-ink-500 dark:text-ink-400"
355
- >
356
- Open a pull request with a YAML file. We review every
357
- submission against the bar — no marketing, no tutorials.
358
- </p>
359
- <a
360
- href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
361
- target="_blank"
362
- rel="noopener noreferrer"
363
- class="btn-primary"
364
- >Submit via GitHub</a
365
- >
366
- </div>
367
- </Container>
368
- </section>
369
- {/* Initial state for the search form is server-rendered; this script
370
- only handles debounced search updates + chip removal so the URL
371
- stays canonical and SSR matches client expectations. */}
372
- <script>
373
- import {
374
- filtersFromSearchParams,
375
- searchParamsFromFilters,
376
- type AppsFilters,
377
- } from "../../lib/apps-search";
378
-
379
- function readFilters(): AppsFilters {
380
- return filtersFromSearchParams(new URLSearchParams(location.search));
381
- }
382
- function writeFilters(f: AppsFilters) {
383
- const sp = searchParamsFromFilters(f);
384
- const qs = sp.toString();
385
- const url = qs ? `?${qs}` : location.pathname;
386
- location.assign(url);
387
- }
388
-
389
- // ── Client-side row filter ─────────────────────────────────────
390
- //
391
- // The page is statically rendered (one HTML for every URL), so
392
- // `?stack=flutter` etc. don't re-render the result list on the
393
- // server. The RefinePanel calls `window.__applyFilters({...})` to
394
- // push a new filter state; we hide non-matching rows, update the
395
- // active-chip strip, the summary line, and the trigger labels
396
- // — all without a full page reload.
397
- //
398
- // Each `.apps-row` already carries `data-stack`, `data-stacks`,
399
- // `data-category`, `data-platforms`, `data-labels`, `data-status`,
400
- // `data-license`, `data-stars`, `data-updated`, `data-name`,
401
- // `data-q` (description, lowercased) for matching.
402
-
403
- type RowEl = HTMLElement & {
404
- dataset: {
405
- stack?: string;
406
- stacks?: string;
407
- category?: string;
408
- platforms?: string;
409
- labels?: string;
410
- status?: string;
411
- license?: string;
412
- stars?: string;
413
- updated?: string;
414
- name?: string;
415
- q?: string;
416
- };
417
- };
418
-
419
- const rows = Array.from(
420
- document.querySelectorAll<RowEl>(".apps-row[data-slug]"),
421
- );
422
- const resultsList = document.getElementById("apps-results-list");
423
- const emptyState = document.getElementById("apps-empty-state");
424
- const resultsCount = document.getElementById("apps-results-count");
425
- const filtersSummary = document.getElementById("apps-filters-summary");
426
- const sortSelect = document.getElementById("sort-select") as HTMLSelectElement | null;
427
- const searchInput = document.getElementById("search-input") as HTMLInputElement | null;
428
- const activeChipsEl = document.getElementById("apps-active-chips");
429
- const clearAllBtn = document.getElementById("apps-clear-all") as HTMLAnchorElement | null;
430
- const triggerLabels = new Map<string, HTMLElement>();
431
-
432
- document.querySelectorAll<HTMLElement>(".apps-filter").forEach((g) => {
433
- const key = g.dataset.filterGroupKey!;
434
- const label = g.querySelector<HTMLElement>(".apps-filter-trigger > span:first-child");
435
- if (label) triggerLabels.set(key, label);
436
- });
437
-
438
- function pillFor(key: string, value: string): HTMLElement {
439
- const el = document.createElement("button");
440
- el.type = "button";
441
- el.className =
442
- "apps-active-chip inline-flex items-center gap-1 rounded-full border border-ink-200 bg-white px-2.5 py-1 text-2xs text-ink-700 transition-colors hover:border-ink-300 dark:border-ink-700 dark:bg-ink-900/40 dark:text-ink-200";
443
- el.dataset.removeKey = key;
444
- el.dataset.removeValue = value;
445
- const v = document.createElement("span");
446
- v.textContent = value;
447
- el.appendChild(v);
448
- const x = document.createElement("span");
449
- x.setAttribute("aria-hidden", "true");
450
- x.textContent = "×";
451
- el.appendChild(x);
452
- return el;
453
- }
454
-
455
- function renderChips(f: AppsFilters) {
456
- if (!activeChipsEl) return;
457
- activeChipsEl.innerHTML = "";
458
- const chips: HTMLElement[] = [];
459
- if (f.q) {
460
- const e = pillFor("q", f.q);
461
- const v = e.querySelector("span")!;
462
- v.textContent = `“${f.q}”`;
463
- chips.push(e);
464
- }
465
- for (const v of f.stacks ?? []) chips.push(pillFor("stacks", v));
466
- for (const v of f.platforms ?? []) chips.push(pillFor("platforms", v));
467
- for (const v of f.categories ?? []) chips.push(pillFor("categories", v));
468
- for (const v of f.labels ?? []) chips.push(pillFor("labels", v));
469
- for (const v of f.licenses ?? []) chips.push(pillFor("licenses", v));
470
- for (const e of chips) activeChipsEl.appendChild(e);
471
- if (clearAllBtn) {
472
- if (chips.length > 0) {
473
- clearAllBtn.classList.remove("hidden");
474
- } else {
475
- clearAllBtn.classList.add("hidden");
476
- }
477
- }
478
- }
479
-
480
- function triggerLabelText(key: string, f: AppsFilters): string {
481
- const map: Record<string, string> = {
482
- stacks: "Stack",
483
- platforms: "Platform",
484
- categories: "Category",
485
- licenses: "License",
486
- };
487
- const base = map[key] ?? key;
488
- const arr = (f as unknown as Record<string, string[] | undefined>)[key] ?? [];
489
- if (arr.length === 0) return `${base}: Any`;
490
- if (arr.length === 1) return `${base}: ${arr[0]}`;
491
- return `${base}: ${arr.length} selected`;
492
- }
493
-
494
- function applyRowFilter(f: AppsFilters): number {
495
- const q = f.q?.trim().toLowerCase() ?? "";
496
- const stacks = new Set(f.stacks ?? []);
497
- const platforms = new Set(f.platforms ?? []);
498
- const categories = new Set(f.categories ?? []);
499
- const labels = new Set(f.labels ?? []);
500
- const licenses = new Set(f.licenses ?? []);
501
- const statuses = new Set(f.statuses ?? []);
502
-
503
- let visible = 0;
504
- for (const row of rows) {
505
- const d = row.dataset;
506
- const rowStacks = (d.stacks || "").split("|").filter(Boolean);
507
- const rowPlatforms = (d.platforms || "").split("|").filter(Boolean);
508
- const rowLabels = (d.labels || "").split("|").filter(Boolean);
509
-
510
- let show = true;
511
- if (q) {
512
- // Match the server-side `filterApps` haystack: name, owner,
513
- // description, category. Without `owner` here, live search
514
- // would miss apps a user is searching for by owner name.
515
- const hay = `${d.name ?? ""} ${d.owner ?? ""} ${d.q ?? ""} ${d.category ?? ""}`;
516
- if (!hay.includes(q)) show = false;
517
- }
518
- if (show && stacks.size > 0) {
519
- if (![d.stack ?? "", ...rowStacks].some((s) => stacks.has(s))) show = false;
520
- }
521
- if (show && platforms.size > 0) {
522
- if (!rowPlatforms.some((p) => platforms.has(p))) show = false;
523
- }
524
- if (show && categories.size > 0) {
525
- if (!categories.has(d.category ?? "")) show = false;
526
- }
527
- if (show && labels.size > 0) {
528
- if (!rowLabels.some((l) => labels.has(l))) show = false;
529
- }
530
- if (show && licenses.size > 0) {
531
- if (!d.license || !licenses.has(d.license)) show = false;
532
- }
533
- if (show && statuses.size > 0) {
534
- if (!d.status || !statuses.has(d.status)) show = false;
535
- }
536
- row.hidden = !show;
537
- if (show) visible++;
538
- }
539
- if (resultsList) resultsList.hidden = visible === 0 && rows.length > 0;
540
- if (emptyState) emptyState.hidden = visible !== 0;
541
- if (resultsCount) {
542
- const total = rows.length;
543
- resultsCount.textContent = `${visible} of ${total}`;
544
- }
545
- return visible;
546
- }
547
-
548
- function renderSummary(f: AppsFilters) {
549
- if (!filtersSummary) return;
550
- const parts: string[] = [];
551
- if (f.q) parts.push(`matching “${f.q}”`);
552
- if (f.stacks?.length) parts.push(`stack: ${f.stacks.join(", ")}`);
553
- if (f.platforms?.length) parts.push(`platform: ${f.platforms.join(", ")}`);
554
- if (f.categories?.length) parts.push(`category: ${f.categories.join(", ")}`);
555
- if (f.licenses?.length) parts.push(`license: ${f.licenses.join(", ")}`);
556
- if (f.labels?.length) parts.push(`label: ${f.labels.join(", ")}`);
557
- filtersSummary.textContent = parts.length ? parts.join(" · ") : "";
558
- }
559
-
560
- function applyAll(f: AppsFilters) {
561
- applyRowFilter(f);
562
- renderChips(f);
563
- renderSummary(f);
564
- for (const [key, el] of triggerLabels) {
565
- el.textContent = triggerLabelText(key, f);
566
- }
567
- if (searchInput && (searchInput.value ?? "") !== (f.q ?? "")) {
568
- searchInput.value = f.q ?? "";
569
- }
570
- if (sortSelect && f.sort && sortSelect.value !== f.sort) {
571
- sortSelect.value = f.sort;
572
- }
573
- }
574
-
575
- // Initial render from URL.
576
- applyAll(readFilters());
577
-
578
- // Hook called by RefinePanel: patch the filter state, push the
579
- // new URL, re-filter rows.
580
- (window as unknown as { __applyFilters: (p: Record<string, string[] | null>) => void }).__applyFilters =
581
- (patch) => {
582
- const f = readFilters();
583
- for (const [key, val] of Object.entries(patch)) {
584
- if (val === null) {
585
- (f as unknown as Record<string, unknown>)[key] = undefined;
586
- } else {
587
- (f as unknown as Record<string, unknown>)[key] = val;
588
- }
589
- }
590
- f.page = 1;
591
- const sp = searchParamsFromFilters(f);
592
- const qs = sp.toString();
593
- const url = qs ? `?${qs}` : location.pathname;
594
- history.pushState({ filters: f }, "", url);
595
- applyAll(f);
596
- };
597
-
598
- // Sort dropdown: navigate to new URL (server-side render).
599
- sortSelect?.addEventListener("change", () => {
600
- const f = readFilters();
601
- f.sort = (sortSelect.value as AppsFilters["sort"]) || undefined;
602
- f.page = 1;
603
- writeFilters(f);
604
- });
605
-
606
- // Active chip removal (delegated) — also works on chips rendered
607
- // by the client-side filter above.
608
- document.addEventListener("click", (e) => {
609
- const target = e.target as HTMLElement;
610
- const removeBtn = target.closest<HTMLElement>(".apps-active-chip");
611
- if (removeBtn) {
612
- e.preventDefault();
613
- const key = removeBtn.dataset.removeKey as keyof AppsFilters;
614
- (window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
615
- [key]: null,
616
- });
617
- return;
618
- }
619
- // Density toggle: behave like a real <button> — anchor was a
620
- // misuse of `aria-pressed` (which belongs on buttons). Now
621
- // we navigate to the URL on click instead of using <a href>.
622
- const densityBtn = target.closest<HTMLButtonElement>("[data-density-toggle]");
623
- if (densityBtn) {
624
- const href = densityBtn.dataset.href;
625
- if (href) location.assign(href);
626
- }
627
- });
628
-
629
- // Search input: debounced client-side patch (no reload).
630
- if (searchInput) {
631
- let timer: number | undefined;
632
- searchInput.addEventListener("input", () => {
633
- if (timer) window.clearTimeout(timer);
634
- timer = window.setTimeout(() => {
635
- (window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
636
- q: searchInput.value.trim() ? [searchInput.value.trim()] : null,
637
- });
638
- }, 220);
639
- });
640
- document.getElementById("search-form")?.addEventListener("submit", (e) => {
641
- e.preventDefault();
642
- (window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
643
- q: searchInput.value.trim() ? [searchInput.value.trim()] : null,
644
- });
645
- });
646
- }
647
-
648
- // Back/forward: re-apply the filter from the new URL.
649
- window.addEventListener("popstate", () => {
650
- applyAll(readFilters());
651
- });
652
- </script>
653
-
654
- <style>
655
- /* Compact density: hide description + chips, keep header /
656
- metadata / actions. Toggle is a single class on the parent
657
- container, so we can flip density without re-rendering rows. */
658
- .apps-density-compact :global(.apps-row) {
659
- padding: 0.625rem 0.875rem;
660
- gap: 0.5rem;
661
- }
662
- .apps-density-compact :global(.apps-row-desc) {
663
- display: none;
664
- }
665
- .apps-density-compact :global(.apps-row-chips) {
666
- display: none;
667
- }
668
- </style>
669
- </BaseLayout>