@grove-dev/astro 0.1.5 → 0.2.5

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +4 -25
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +56 -22
  5. package/dist/index.js.map +1 -1
  6. package/dist/layouts/index.d.ts +25 -0
  7. package/dist/layouts/index.d.ts.map +1 -0
  8. package/dist/layouts/index.js +25 -0
  9. package/dist/layouts/index.js.map +1 -0
  10. package/dist/lib/display.d.ts +33 -0
  11. package/dist/lib/display.d.ts.map +1 -0
  12. package/dist/lib/display.js +104 -0
  13. package/dist/lib/display.js.map +1 -0
  14. package/dist/lib/format.d.ts +30 -0
  15. package/dist/lib/format.d.ts.map +1 -0
  16. package/dist/lib/format.js +73 -0
  17. package/dist/lib/format.js.map +1 -0
  18. package/dist/lib/index.d.ts +26 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/index.js +26 -0
  21. package/dist/lib/index.js.map +1 -0
  22. package/dist/lib/lenses.d.ts +96 -0
  23. package/dist/lib/lenses.d.ts.map +1 -0
  24. package/dist/lib/lenses.js +181 -0
  25. package/dist/lib/lenses.js.map +1 -0
  26. package/dist/lib/repo.d.ts +30 -0
  27. package/dist/lib/repo.d.ts.map +1 -0
  28. package/dist/lib/repo.js +44 -0
  29. package/dist/lib/repo.js.map +1 -0
  30. package/dist/lib/scores.d.ts +38 -0
  31. package/dist/lib/scores.d.ts.map +1 -0
  32. package/dist/lib/scores.js +83 -0
  33. package/dist/lib/scores.js.map +1 -0
  34. package/dist/lib/search.d.ts +199 -0
  35. package/dist/lib/search.d.ts.map +1 -0
  36. package/dist/lib/search.js +357 -0
  37. package/dist/lib/search.js.map +1 -0
  38. package/dist/lib/taxonomy-counts.d.ts +25 -0
  39. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  40. package/dist/lib/taxonomy-counts.js +41 -0
  41. package/dist/lib/taxonomy-counts.js.map +1 -0
  42. package/package.json +7 -7
  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 +306 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RecordSection.astro +88 -0
  56. package/src/components/RefinePanel.astro +197 -0
  57. package/src/components/ScoreBars.astro +92 -22
  58. package/src/components/SmartLensTabs.astro +71 -0
  59. package/src/components/StackGrid.astro +132 -0
  60. package/src/components/WhyThisExists.astro +116 -0
  61. package/src/index.ts +61 -14
  62. package/src/layouts/BaseLayout.astro +227 -15
  63. package/src/layouts/Container.astro +57 -0
  64. package/src/layouts/Footer.astro +235 -0
  65. package/src/layouts/Header.astro +159 -0
  66. package/src/layouts/SectionHeader.astro +58 -0
  67. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  68. package/src/layouts/ThemeToggle.astro +110 -0
  69. package/src/layouts/astro-shim.d.ts +29 -0
  70. package/src/layouts/index.ts +24 -0
  71. package/src/lib/display.ts +118 -0
  72. package/src/lib/format.ts +65 -0
  73. package/src/lib/index.ts +26 -0
  74. package/src/lib/lenses.ts +241 -0
  75. package/src/lib/repo.ts +44 -0
  76. package/src/lib/scores.ts +94 -0
  77. package/src/lib/search.ts +481 -0
  78. package/src/lib/taxonomy-counts.ts +60 -0
  79. package/src/styles.css +366 -638
  80. package/templates/default/astro.config.mjs +8 -6
  81. package/templates/default/content/pages/about.md +66 -0
  82. package/templates/default/content/pages/methodology.md +103 -0
  83. package/templates/default/data/generated/records.full.json +8 -0
  84. package/templates/default/data/generated/records.index.json +7 -0
  85. package/templates/default/data/generated/records.json +8 -0
  86. package/templates/default/data/generated/site-config.json +31 -0
  87. package/templates/default/data/records/.gitkeep +0 -0
  88. package/templates/default/grove.config.ts +36 -0
  89. package/templates/default/package.json +6 -5
  90. package/templates/default/public/llms-full.txt +14 -0
  91. package/templates/default/public/llms.txt +11 -0
  92. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  93. package/templates/default/src/components/layout/Footer.astro +16 -134
  94. package/templates/default/src/components/layout/Header.astro +25 -126
  95. package/templates/default/src/data/records.ts +193 -0
  96. package/templates/default/src/lib/markdown.ts +171 -0
  97. package/templates/default/src/pages/about.astro +95 -429
  98. package/templates/default/src/pages/index.astro +263 -61
  99. package/templates/default/src/pages/projects/[slug].astro +480 -0
  100. package/templates/default/src/pages/projects/index.astro +193 -0
  101. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  102. package/templates/default/src/pages/submit.astro +122 -411
  103. package/templates/default/src/styles/global.css +42 -68
  104. package/src/components/ActiveFilterChips.astro +0 -20
  105. package/src/components/DirectoryFilters.astro +0 -100
  106. package/src/components/DirectoryHero.astro +0 -32
  107. package/src/components/HealthBadge.astro +0 -12
  108. package/src/components/LensTabs.astro +0 -26
  109. package/src/components/MethodologyPanel.astro +0 -20
  110. package/src/components/Pagination.astro +0 -24
  111. package/src/components/ProjectDetail.astro +0 -166
  112. package/src/components/SubmitDraft.astro +0 -137
  113. package/templates/default/src/components/AppCard.astro +0 -248
  114. package/templates/default/src/components/AppSection.astro +0 -58
  115. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  116. package/templates/default/src/components/AppsPagination.astro +0 -147
  117. package/templates/default/src/components/CategoryGrid.astro +0 -80
  118. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  119. package/templates/default/src/components/DecisionRow.astro +0 -219
  120. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  121. package/templates/default/src/components/ExploreByStack.astro +0 -102
  122. package/templates/default/src/components/Hero.astro +0 -176
  123. package/templates/default/src/components/MinimalAbout.astro +0 -57
  124. package/templates/default/src/components/OriginalCollection.astro +0 -144
  125. package/templates/default/src/components/RefinePanel.astro +0 -272
  126. package/templates/default/src/components/ScoreBars.astro +0 -72
  127. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  128. package/templates/default/src/components/StackGrid.astro +0 -95
  129. package/templates/default/src/components/WhyThisExists.astro +0 -81
  130. package/templates/default/src/components/icons/Icon.astro +0 -148
  131. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  132. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  133. package/templates/default/src/data/apps.ts +0 -149
  134. package/templates/default/src/data/categories.ts +0 -27
  135. package/templates/default/src/data/config.ts +0 -28
  136. package/templates/default/src/data/contributors.ts +0 -96
  137. package/templates/default/src/data/stacks.ts +0 -47
  138. package/templates/default/src/data/stats.ts +0 -65
  139. package/templates/default/src/data/types.ts +0 -248
  140. package/templates/default/src/lib/apps-search.ts +0 -228
  141. package/templates/default/src/lib/format.ts +0 -36
  142. package/templates/default/src/lib/lenses.ts +0 -98
  143. package/templates/default/src/lib/repo.ts +0 -12
  144. package/templates/default/src/lib/scores.ts +0 -51
  145. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  146. package/templates/default/src/pages/apps/[slug].astro +0 -709
  147. package/templates/default/src/pages/apps/index.astro +0 -669
  148. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,176 +0,0 @@
1
- ---
2
- import { siteConfig } from "../data/config";
3
- import Container from "./layout/Container.astro";
4
- import { stats } from "../data/stats";
5
- import { compact } from "../lib/format";
6
-
7
- const githubUrl = stats.originalRepo;
8
-
9
- const trustStats: { value: string; label: string }[] = [
10
- { value: compact(stats.apps), label: "apps indexed" },
11
- { value: String(stats.categories), label: "categories" },
12
- { value: String(stats.stacks), label: "stacks" },
13
- { value: compact(stats.contributors), label: "contributors" },
14
- { value: compact(stats.stars), label: "GitHub stars" },
15
- ];
16
- ---
17
-
18
- <section class="relative overflow-hidden border-b border-ink-200 dark:border-ink-800">
19
- {/* Soft animated background gradient — subtle, not loud. */}
20
- <div
21
- aria-hidden="true"
22
- class="pointer-events-none absolute inset-0 opacity-60 dark:opacity-40"
23
- style="
24
- background-image:
25
- radial-gradient(60% 50% at 15% 20%, rgba(31, 111, 235, 0.12), transparent 60%),
26
- radial-gradient(50% 40% at 85% 10%, rgba(110, 231, 183, 0.10), transparent 60%),
27
- radial-gradient(40% 40% at 50% 90%, rgba(251, 146, 60, 0.08), transparent 60%);
28
- "
29
- ></div>
30
-
31
- <Container>
32
- <div class="relative py-14 sm:py-20">
33
- <div class="max-w-3xl">
34
- <span
35
- class="inline-flex animate-fade-in items-center gap-2 text-2xs font-medium text-ink-500 dark:text-ink-400"
36
- >
37
- <span
38
- aria-hidden="true"
39
- class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"
40
- ></span>
41
- A directory of real open-source apps
42
- <span
43
- class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-700 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
44
- aria-label="Pre-1.0 — data and UI are still being curated. Expect rough edges."
45
- >
46
- Beta
47
- </span>
48
- <span class="text-ink-400 dark:text-ink-500">·</span>
49
- <span>v0.1</span>
50
- </span>
51
-
52
- <h1
53
- class="mt-3 animate-fade-in-up text-[2.25rem] sm:text-[2.75rem] md:text-[3rem] font-semibold tracking-[-0.02em] leading-[1.05] text-ink-900 dark:text-ink-100 m-0"
54
- style="animation-delay: 0.05s;"
55
- >
56
- Find open-source apps you can actually<br class="hidden sm:block" />
57
- <span
58
- class="bg-gradient-to-r from-ink-900 via-ink-700 to-ink-900 bg-clip-text text-transparent dark:from-ink-100 dark:via-ink-300 dark:to-ink-100"
59
- style="background-size: 200% 200%; animation: fade-in-up 0.7s ease-out 0.1s both, gradient-shift 8s ease-in-out 1s infinite;"
60
- >
61
- run, study, and contribute to.
62
- </span>
63
- </h1>
64
-
65
- <p
66
- class="mt-4 animate-fade-in-up text-[0.9375rem] sm:text-[1rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
67
- style="animation-delay: 0.15s;"
68
- >
69
- {siteConfig.name} is a curated directory of real applications with public source code.
70
- Browse by stack, category, maturity, platform, license, stars, and activity — without
71
- digging through random GitHub searches and outdated README lists.
72
- </p>
73
- </div>
74
-
75
- {/* Trust stats row — the single most credibility-critical block on the
76
- page. Number + label, monospaced numbers so they line up. */}
77
- <dl
78
- class="mt-7 grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-5"
79
- aria-label="Directory at a glance"
80
- >
81
- {
82
- trustStats.map((s) => (
83
- <div class="flex flex-col gap-0.5">
84
- <dt class="sr-only">{s.label}</dt>
85
- <dd class="m-0 font-mono text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
86
- {s.value}
87
- </dd>
88
- <dd class="m-0 text-2xs uppercase tracking-wider text-ink-500 dark:text-ink-400">
89
- {s.label}
90
- </dd>
91
- </div>
92
- ))
93
- }
94
- </dl>
95
-
96
- {/* Search + filters */}
97
- <form
98
- class="mt-7 flex flex-col gap-2 sm:flex-row sm:items-stretch"
99
- role="search"
100
- action="/apps"
101
- method="get"
102
- >
103
- <label class="relative flex-1">
104
- <span class="sr-only">Search apps, stacks, categories, licenses</span>
105
- <svg
106
- viewBox="0 0 16 16"
107
- width="16"
108
- height="16"
109
- aria-hidden="true"
110
- class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-400"
111
- fill="currentColor"
112
- >
113
- <path
114
- 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"
115
- ></path>
116
- </svg>
117
- <input
118
- type="search"
119
- name="q"
120
- placeholder="Search apps, stacks, categories, licenses, platforms..."
121
- class="h-10 w-full rounded-md border border-ink-200 bg-white pl-10 pr-3 text-[0.9375rem] 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"
122
- />
123
- </label>
124
- <a
125
- href="/apps"
126
- class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md border border-ink-200 bg-white px-3 text-[0.875rem] font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
127
- aria-label="Browse all apps with filters"
128
- >
129
- <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
130
- <path d="M.75 3h14.5a.75.75 0 0 1 .55 1.26l-5.55 6.1v4.39a.75.75 0 0 1-1.2.6l-2.5-1.87a.75.75 0 0 1-.3-.6v-2.52L.2 4.26A.75.75 0 0 1 .75 3Z"/>
131
- </svg>
132
- <span>Filters</span>
133
- </a>
134
- <button
135
- type="submit"
136
- class="btn-primary h-10 px-4 sm:px-5"
137
- >
138
- Search
139
- </button>
140
- </form>
141
-
142
- <p
143
- class="mt-2 text-2xs text-ink-500 dark:text-ink-400 m-0"
144
- aria-hidden="true"
145
- >
146
- Try{" "}
147
- <a href="/apps?stack=Flutter" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Flutter apps</a>
148
- <span class="mx-1">·</span>
149
- <a href="/apps?category=Finance" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Finance</a>
150
- <span class="mx-1">·</span>
151
- <a href="/apps?label=hot" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Hot projects</a>
152
- <span class="mx-1">·</span>
153
- <a href="/apps?platform=Android" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Android</a>
154
- <span class="mx-1">·</span>
155
- <a href="/apps?label=mature" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Mature</a>
156
- </p>
157
-
158
- <div class="mt-5 flex flex-wrap items-center gap-2">
159
- <a href="#new" class="btn-primary">Browse apps</a>
160
- <a
161
- href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
162
- target="_blank"
163
- rel="noopener noreferrer"
164
- class="btn-outline">Submit via GitHub</a
165
- >
166
- <a
167
- href={githubUrl}
168
- target="_blank"
169
- rel="noopener noreferrer"
170
- class="btn-ghost"
171
- >View source on GitHub →</a
172
- >
173
- </div>
174
- </div>
175
- </Container>
176
- </section>
@@ -1,57 +0,0 @@
1
- ---
2
- import { siteConfig } from "../data/config";
3
- import Container from "./layout/Container.astro";
4
-
5
- const points = [
6
- {
7
- title: "Real codebases",
8
- body: "Not just tutorials or packages — apps that actually run.",
9
- },
10
- {
11
- title: "Searchable context",
12
- body: "Find apps by stack, platform, category, license, and activity.",
13
- },
14
- {
15
- title: "Open data",
16
- body: "Static site. GitHub-based data. Community maintained.",
17
- },
18
- ];
19
- ---
20
-
21
- <section id="about" class="section scroll-mt-20">
22
- <Container size="narrow">
23
- <span
24
- class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
25
- >About</span
26
- >
27
- <h2
28
- class="mt-1 text-[1.5rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
29
- >
30
- Why this exists
31
- </h2>
32
- <p
33
- class="mt-3 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0"
34
- >
35
- Most app examples are either toy projects, abandoned lists, or hard to
36
- search. {siteConfig.name} focuses on real application codebases that
37
- developers can study, compare, and contribute to.
38
- </p>
39
-
40
- <ul
41
- class="mt-6 grid list-none grid-cols-1 gap-3 p-0 m-0 sm:grid-cols-3"
42
- >
43
- {
44
- points.map((p) => (
45
- <li class="card p-4">
46
- <h3 class="m-0 text-[0.875rem] font-semibold text-ink-900 dark:text-ink-100">
47
- {p.title}
48
- </h3>
49
- <p class="mt-1 m-0 text-2xs leading-relaxed text-ink-500 dark:text-ink-400">
50
- {p.body}
51
- </p>
52
- </li>
53
- ))
54
- }
55
- </ul>
56
- </Container>
57
- </section>
@@ -1,144 +0,0 @@
1
- ---
2
- import type { SiteStats } from "../data/stats";
3
- import Container from "./layout/Container.astro";
4
-
5
- interface Props {
6
- stats: SiteStats;
7
- }
8
-
9
- const { stats } = Astro.props;
10
- const repoUrl = stats.originalRepo;
11
-
12
- function formatCount(n: number): string {
13
- if (n >= 1000) {
14
- const k = n / 1000;
15
- return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
16
- }
17
- return n.toString();
18
- }
19
-
20
- const starsLabel = formatCount(stats.stars);
21
- const forksLabel = formatCount(stats.forks);
22
- ---
23
-
24
- <section
25
- id="original"
26
- class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800"
27
- >
28
- <Container>
29
- <div
30
- class="card flex flex-col gap-6 p-6 sm:flex-row sm:items-center sm:justify-between sm:gap-8"
31
- >
32
- <div class="max-w-2xl">
33
- <span
34
- class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
35
- >Origin</span
36
- >
37
- <h2
38
- class="mt-1 text-[1.375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
39
- >
40
- Started from Open Source Flutter Apps.
41
- </h2>
42
- <p
43
- class="mt-2 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0"
44
- >
45
- The project began as a curated Flutter app list and is now expanding
46
- into a broader open-source app directory, starting with mobile.
47
- </p>
48
-
49
- <dl class="mt-4 flex flex-wrap items-center gap-x-6 gap-y-2">
50
- <div class="flex items-center gap-2">
51
- <svg
52
- viewBox="0 0 16 16"
53
- width="14"
54
- height="14"
55
- aria-hidden="true"
56
- fill="currentColor"
57
- class="text-amber-500"
58
- >
59
- <path
60
- d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"
61
- />
62
- </svg>
63
- <dd class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
64
- <span class="font-mono">{starsLabel}+</span>
65
- <span class="ml-1 text-2xs font-normal text-ink-500 dark:text-ink-400">stars</span>
66
- </dd>
67
- </div>
68
- <div class="flex items-center gap-2">
69
- <svg
70
- viewBox="0 0 16 16"
71
- width="14"
72
- height="14"
73
- aria-hidden="true"
74
- fill="none"
75
- stroke="currentColor"
76
- stroke-width="1.5"
77
- class="text-ink-500 dark:text-ink-400"
78
- >
79
- <circle cx="4" cy="3" r="1.5"/>
80
- <circle cx="12" cy="3" r="1.5"/>
81
- <circle cx="8" cy="8" r="1.5"/>
82
- <circle cx="4" cy="13" r="1.5"/>
83
- <circle cx="12" cy="13" r="1.5"/>
84
- <path d="M5 4.2 7 6.8M11 4.2 9 6.8M5 11.8 7 9.2M11 11.8 9 9.2"/>
85
- </svg>
86
- <dd class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
87
- <span class="font-mono">{forksLabel}+</span>
88
- <span class="ml-1 text-2xs font-normal text-ink-500 dark:text-ink-400">forks</span>
89
- </dd>
90
- </div>
91
- <div class="flex items-center gap-2">
92
- <svg
93
- viewBox="0 0 16 16"
94
- width="14"
95
- height="14"
96
- aria-hidden="true"
97
- fill="none"
98
- stroke="currentColor"
99
- stroke-width="1.5"
100
- class="text-ink-500 dark:text-ink-400"
101
- >
102
- <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 0 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Z"/>
103
- <path d="M5 6.5a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 5 6.5Zm.75 2.25a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5Z"/>
104
- </svg>
105
- <dd class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
106
- <span class="font-mono">{stats.contributors}+</span>
107
- <span class="ml-1 text-2xs font-normal text-ink-500 dark:text-ink-400">contributors</span>
108
- </dd>
109
- </div>
110
- </dl>
111
- </div>
112
-
113
- <div class="flex shrink-0 flex-col gap-2 sm:items-end">
114
- <a
115
- href={repoUrl}
116
- target="_blank"
117
- rel="noopener noreferrer"
118
- class="btn-primary"
119
- >
120
- <svg
121
- viewBox="0 0 16 16"
122
- width="14"
123
- height="14"
124
- aria-hidden="true"
125
- fill="currentColor"
126
- >
127
- <path
128
- d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"
129
- />
130
- </svg>
131
- View on GitHub
132
- </a>
133
- <a
134
- href={`${repoUrl}/blob/main/CONTRIBUTING.md`}
135
- target="_blank"
136
- rel="noopener noreferrer"
137
- class="text-2xs text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
138
- >
139
- How to contribute →
140
- </a>
141
- </div>
142
- </div>
143
- </Container>
144
- </section>
@@ -1,272 +0,0 @@
1
- ---
2
- import type { AppsFilters } from "../lib/apps-search";
3
-
4
- interface FacetOption {
5
- value: string;
6
- count: number;
7
- }
8
-
9
- interface Props {
10
- facets: {
11
- stacks: FacetOption[];
12
- platforms: FacetOption[];
13
- categories: FacetOption[];
14
- labels: FacetOption[];
15
- licenses: FacetOption[];
16
- };
17
- initial: AppsFilters;
18
- class?: string;
19
- }
20
-
21
- const { facets, initial, class: className = "" } = Astro.props;
22
-
23
- /**
24
- * A single filter group rendered as a dropdown trigger + popover.
25
- * Multi-select by default; license is intentionally single-select.
26
- */
27
- interface FilterGroup {
28
- /** Short label, e.g. "Stack". */
29
- label: string;
30
- /** Key in AppsFilters (plural). */
31
- filterKey: "stacks" | "platforms" | "categories" | "labels" | "licenses";
32
- /** Options with counts. */
33
- options: FacetOption[];
34
- initial: string[];
35
- /** Single-select (radio-like) — for license, which is usually one. */
36
- single?: boolean;
37
- }
38
-
39
- const groups: FilterGroup[] = [
40
- { label: "Stack", filterKey: "stacks", options: facets.stacks, initial: initial.stacks ?? [] },
41
- { label: "Platform", filterKey: "platforms", options: facets.platforms, initial: initial.platforms ?? [] },
42
- { label: "Category", filterKey: "categories", options: facets.categories, initial: initial.categories ?? [] },
43
- { label: "License", filterKey: "licenses", options: facets.licenses, initial: initial.licenses ?? [], single: true },
44
- ];
45
-
46
- /** Build the trigger label: "Stack" when empty, "Stack: 2" when 2 selected. */
47
- function triggerLabel(g: FilterGroup): string {
48
- const n = g.initial.length;
49
- if (n === 0) return `${g.label}: Any`;
50
- if (n === 1) return `${g.label}: ${g.initial[0]}`;
51
- return `${g.label}: ${n} selected`;
52
- }
53
- ---
54
-
55
- <section class={`${className}`}>
56
- <div
57
- class="flex flex-wrap items-center gap-2"
58
- role="toolbar"
59
- aria-label="Filter apps"
60
- >
61
- {
62
- groups.map((g) => {
63
- const hasActive = g.initial.length > 0;
64
- const popoverId = `filter-popover-${g.filterKey}`;
65
- return (
66
- <div
67
- class="apps-filter relative"
68
- data-filter-group-key={g.filterKey}
69
- data-filter-single={g.single ? "true" : "false"}
70
- >
71
- <button
72
- type="button"
73
- class={`apps-filter-trigger inline-flex h-9 items-center gap-1.5 rounded-md border px-3 text-[0.8125rem] font-medium transition-colors ${
74
- hasActive
75
- ? "border-ink-900 bg-ink-900 text-white dark:border-ink-100 dark:bg-ink-100 dark:text-ink-900"
76
- : "border-ink-200 bg-white text-ink-700 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"
77
- }`}
78
- aria-haspopup="listbox"
79
- aria-expanded="false"
80
- aria-controls={popoverId}
81
- data-filter-trigger
82
- >
83
- <span>{triggerLabel(g)}</span>
84
- <svg
85
- viewBox="0 0 16 16"
86
- width="11"
87
- height="11"
88
- aria-hidden="true"
89
- fill="currentColor"
90
- class="opacity-70"
91
- >
92
- <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"/>
93
- </svg>
94
- </button>
95
- <div
96
- id={popoverId}
97
- class="apps-filter-popover absolute left-0 top-full z-40 mt-1 hidden w-72 max-w-[calc(100vw-2rem)] rounded-lg border border-ink-200 bg-white p-1.5 shadow-lg dark:border-ink-800 dark:bg-ink-950"
98
- role="listbox"
99
- aria-multiselectable={g.single ? "false" : "true"}
100
- hidden
101
- >
102
- {g.options.length === 0 ? (
103
- <p class="m-0 px-2 py-1.5 text-2xs text-ink-400 dark:text-ink-500">
104
- No options available.
105
- </p>
106
- ) : (
107
- <ul class="m-0 flex max-h-72 flex-col gap-0.5 overflow-y-auto p-0" data-filter-list>
108
- {g.options.map((opt) => {
109
- const checked = g.initial.includes(opt.value);
110
- return (
111
- <li>
112
- <label
113
- class={`flex cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-[0.8125rem] transition-colors hover:bg-ink-50 dark:hover:bg-ink-900/60 ${
114
- checked
115
- ? "text-ink-900 dark:text-ink-100"
116
- : "text-ink-700 dark:text-ink-300"
117
- }`}
118
- >
119
- <span class="flex min-w-0 items-center gap-2">
120
- <input
121
- type={g.single ? "radio" : "checkbox"}
122
- class="apps-filter-input h-3.5 w-3.5 shrink-0 cursor-pointer accent-ink-900 dark:accent-ink-100"
123
- data-filter-key={g.filterKey}
124
- data-filter-value={opt.value}
125
- data-filter-single={g.single ? "true" : undefined}
126
- checked={checked}
127
- />
128
- <span class="truncate">{opt.value}</span>
129
- </span>
130
- <span class="shrink-0 font-mono text-2xs tabular-nums text-ink-500 dark:text-ink-400">
131
- {opt.count}
132
- </span>
133
- </label>
134
- </li>
135
- );
136
- })}
137
- </ul>
138
- )}
139
- <div class="mt-1 flex items-center justify-between border-t border-ink-100 px-2 pt-1.5 dark:border-ink-900">
140
- <button
141
- type="button"
142
- class="apps-filter-clear text-2xs font-medium text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
143
- hidden={!hasActive}
144
- >
145
- Clear {g.label.toLowerCase()}
146
- </button>
147
- <button
148
- type="button"
149
- class="apps-filter-apply ml-auto rounded-md bg-ink-900 px-2.5 py-1 text-2xs font-medium text-white transition-colors hover:bg-ink-700 dark:bg-ink-100 dark:text-ink-900 dark:hover:bg-ink-300"
150
- >
151
- Apply
152
- </button>
153
- </div>
154
- </div>
155
- </div>
156
- );
157
- })
158
- }
159
- </div>
160
- </section>
161
-
162
- <script>
163
- /**
164
- * Dropdown behavior for each filter group:
165
- * - click trigger → toggle popover
166
- * - click outside / ESC → close
167
- * - Apply / Clear / Single-select fire `applyFilterPatch()` which
168
- * is a host-page hook (defined in `src/pages/apps/index.astro`).
169
- * The hook updates the URL via `history.pushState` (no reload)
170
- * and re-runs the row filter so the result list updates
171
- * immediately. Static pages don't filter on the server, so
172
- * this is the only way the filter actually narrows the list.
173
- */
174
- function applyFilterPatch(patch: Record<string, string[] | null>) {
175
- const fn = (window as unknown as { __applyFilters?: (p: typeof patch) => void }).__applyFilters;
176
- if (typeof fn === "function") fn(patch);
177
- else location.assign(location.pathname); // graceful fallback
178
- }
179
-
180
- (() => {
181
- const groups = document.querySelectorAll<HTMLElement>(".apps-filter");
182
- if (!groups.length) return;
183
-
184
- function closeAll(except?: HTMLElement) {
185
- groups.forEach((g) => {
186
- if (g === except) return;
187
- const pop = g.querySelector<HTMLElement>(".apps-filter-popover");
188
- const trig = g.querySelector<HTMLElement>(".apps-filter-trigger");
189
- if (!pop || !trig) return;
190
- pop.hidden = true;
191
- pop.classList.add("hidden");
192
- trig.setAttribute("aria-expanded", "false");
193
- });
194
- }
195
-
196
- function openGroup(g: HTMLElement) {
197
- const pop = g.querySelector<HTMLElement>(".apps-filter-popover");
198
- const trig = g.querySelector<HTMLElement>(".apps-filter-trigger");
199
- if (!pop || !trig) return;
200
- pop.hidden = false;
201
- pop.classList.remove("hidden");
202
- trig.setAttribute("aria-expanded", "true");
203
- }
204
-
205
- function closeGroup(g: HTMLElement) {
206
- const pop = g.querySelector<HTMLElement>(".apps-filter-popover");
207
- const trig = g.querySelector<HTMLElement>(".apps-filter-trigger");
208
- if (!pop || !trig) return;
209
- pop.hidden = true;
210
- pop.classList.add("hidden");
211
- trig.setAttribute("aria-expanded", "false");
212
- }
213
-
214
- groups.forEach((g) => {
215
- const trig = g.querySelector<HTMLElement>(".apps-filter-trigger");
216
- const pop = g.querySelector<HTMLElement>(".apps-filter-popover");
217
- const applyBtn = g.querySelector<HTMLElement>(".apps-filter-apply");
218
- const clearBtn = g.querySelector<HTMLElement>(".apps-filter-clear");
219
- const isSingle = g.dataset.filterSingle === "true";
220
- if (!trig || !pop) return;
221
-
222
- trig.addEventListener("click", (e) => {
223
- e.stopPropagation();
224
- const isOpen = trig.getAttribute("aria-expanded") === "true";
225
- closeAll(isOpen ? undefined : g);
226
- if (isOpen) closeGroup(g);
227
- else openGroup(g);
228
- });
229
-
230
- // Single-select: click any input immediately applies + closes.
231
- if (isSingle) {
232
- pop.querySelectorAll<HTMLInputElement>(".apps-filter-input").forEach((input) => {
233
- input.addEventListener("change", () => {
234
- const key = input.dataset.filterKey!;
235
- const value = input.dataset.filterValue!;
236
- applyFilterPatch({ [key]: [value] });
237
- });
238
- });
239
- }
240
-
241
- // Apply: read checked inputs, write URL.
242
- applyBtn?.addEventListener("click", (e) => {
243
- e.stopPropagation();
244
- const key = g.dataset.filterGroupKey!;
245
- const inputs = pop.querySelectorAll<HTMLInputElement>(".apps-filter-input");
246
- const chosen: string[] = [];
247
- inputs.forEach((i) => {
248
- if (i.checked) chosen.push(i.dataset.filterValue!);
249
- });
250
- applyFilterPatch({ [key]: chosen });
251
- });
252
-
253
- // Clear: uncheck all in this group, remove from URL.
254
- clearBtn?.addEventListener("click", (e) => {
255
- e.stopPropagation();
256
- const key = g.dataset.filterGroupKey!;
257
- applyFilterPatch({ [key]: null });
258
- });
259
-
260
- // Prevent clicks inside the popover from closing it (e.g. the
261
- // scrollbar area).
262
- pop.addEventListener("click", (e) => e.stopPropagation());
263
- });
264
-
265
- // Close on outside click.
266
- document.addEventListener("click", () => closeAll());
267
- // Close on ESC.
268
- document.addEventListener("keydown", (e) => {
269
- if (e.key === "Escape") closeAll();
270
- });
271
- })();
272
- </script>