@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +306 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RecordSection.astro +88 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +193 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +263 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- 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>
|