@grove-dev/astro 0.1.0
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/README.md +81 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/components/ActiveFilterChips.astro +20 -0
- package/src/components/CategoryGrid.astro +27 -0
- package/src/components/DirectoryFilters.astro +100 -0
- package/src/components/DirectoryHero.astro +32 -0
- package/src/components/HealthBadge.astro +12 -0
- package/src/components/ItemCard.astro +41 -0
- package/src/components/LensTabs.astro +26 -0
- package/src/components/MethodologyPanel.astro +20 -0
- package/src/components/Pagination.astro +24 -0
- package/src/components/ProjectDetail.astro +166 -0
- package/src/components/ScoreBars.astro +32 -0
- package/src/components/SubmitDraft.astro +137 -0
- package/src/index.ts +17 -0
- package/src/layouts/BaseLayout.astro +32 -0
- package/src/styles.css +675 -0
- package/templates/default/astro.config.mjs +23 -0
- package/templates/default/data/generated/apps.json +1 -0
- package/templates/default/data/generated/repo-stats.json +1 -0
- package/templates/default/package.json +35 -0
- package/templates/default/public/icons/platforms/android.svg +1 -0
- package/templates/default/public/icons/platforms/apple-dark.svg +1 -0
- package/templates/default/public/icons/platforms/apple-light.svg +1 -0
- package/templates/default/public/icons/platforms/chrome.svg +1 -0
- package/templates/default/public/icons/platforms/chromeos.svg +5 -0
- package/templates/default/public/icons/platforms/desktop.svg +4 -0
- package/templates/default/public/icons/platforms/embedded.svg +5 -0
- package/templates/default/public/icons/platforms/ios.svg +1 -0
- package/templates/default/public/icons/platforms/linux.svg +1 -0
- package/templates/default/public/icons/platforms/macos.svg +1 -0
- package/templates/default/public/icons/platforms/ubuntu.svg +1 -0
- package/templates/default/public/icons/platforms/web.svg +5 -0
- package/templates/default/public/icons/platforms/windows.svg +6 -0
- package/templates/default/public/icons/stacks/android.svg +1 -0
- package/templates/default/public/icons/stacks/apple-dark.svg +1 -0
- package/templates/default/public/icons/stacks/apple-light.svg +1 -0
- package/templates/default/public/icons/stacks/capacitor.svg +14 -0
- package/templates/default/public/icons/stacks/dart.svg +1 -0
- package/templates/default/public/icons/stacks/firebase.svg +1 -0
- package/templates/default/public/icons/stacks/flutter.svg +1 -0
- package/templates/default/public/icons/stacks/graphql.svg +1 -0
- package/templates/default/public/icons/stacks/ionic.svg +4 -0
- package/templates/default/public/icons/stacks/java.svg +1 -0
- package/templates/default/public/icons/stacks/javascript.svg +1 -0
- package/templates/default/public/icons/stacks/kotlin.svg +1 -0
- package/templates/default/public/icons/stacks/mongodb.svg +1 -0
- package/templates/default/public/icons/stacks/nodejs.svg +1 -0
- package/templates/default/public/icons/stacks/python.svg +1 -0
- package/templates/default/public/icons/stacks/react-native.svg +1 -0
- package/templates/default/public/icons/stacks/react.svg +1 -0
- package/templates/default/public/icons/stacks/rust.svg +1 -0
- package/templates/default/public/icons/stacks/solidity.svg +1 -0
- package/templates/default/public/icons/stacks/swift.svg +1 -0
- package/templates/default/public/icons/stacks/tensorflow.svg +1 -0
- package/templates/default/public/icons/stacks/typescript.svg +1 -0
- package/templates/default/public/og-image.svg +48 -0
- package/templates/default/public/robots.txt +53 -0
- package/templates/default/src/components/AppCard.astro +248 -0
- package/templates/default/src/components/AppSection.astro +58 -0
- package/templates/default/src/components/AppsIndexRow.astro +230 -0
- package/templates/default/src/components/AppsPagination.astro +147 -0
- package/templates/default/src/components/CategoryGrid.astro +80 -0
- package/templates/default/src/components/ContributorsGrid.astro +105 -0
- package/templates/default/src/components/DecisionRow.astro +219 -0
- package/templates/default/src/components/ExploreByCategory.astro +74 -0
- package/templates/default/src/components/ExploreByStack.astro +102 -0
- package/templates/default/src/components/Hero.astro +176 -0
- package/templates/default/src/components/MinimalAbout.astro +57 -0
- package/templates/default/src/components/OriginalCollection.astro +144 -0
- package/templates/default/src/components/RefinePanel.astro +272 -0
- package/templates/default/src/components/ScoreBars.astro +72 -0
- package/templates/default/src/components/SmartLensTabs.astro +65 -0
- package/templates/default/src/components/StackGrid.astro +95 -0
- package/templates/default/src/components/WhyThisExists.astro +81 -0
- package/templates/default/src/components/icons/Icon.astro +148 -0
- package/templates/default/src/components/layout/BaseLayout.astro +94 -0
- package/templates/default/src/components/layout/Container.astro +18 -0
- package/templates/default/src/components/layout/Footer.astro +145 -0
- package/templates/default/src/components/layout/Header.astro +144 -0
- package/templates/default/src/components/layout/SectionHeader.astro +42 -0
- package/templates/default/src/components/layout/Seo.astro +120 -0
- package/templates/default/src/components/layout/ThemeToggle.astro +97 -0
- package/templates/default/src/data/apps.ts +149 -0
- package/templates/default/src/data/categories.ts +27 -0
- package/templates/default/src/data/config.ts +28 -0
- package/templates/default/src/data/contributors.ts +96 -0
- package/templates/default/src/data/stacks.ts +47 -0
- package/templates/default/src/data/stats.ts +65 -0
- package/templates/default/src/data/types.ts +248 -0
- package/templates/default/src/lib/apps-search.ts +228 -0
- package/templates/default/src/lib/format.ts +36 -0
- package/templates/default/src/lib/lenses.ts +98 -0
- package/templates/default/src/lib/repo.ts +12 -0
- package/templates/default/src/lib/scores.ts +51 -0
- package/templates/default/src/lib/taxonomy-counts.ts +31 -0
- package/templates/default/src/pages/about.astro +433 -0
- package/templates/default/src/pages/apps/[slug].astro +709 -0
- package/templates/default/src/pages/apps/index.astro +669 -0
- package/templates/default/src/pages/contributors.astro +142 -0
- package/templates/default/src/pages/index.astro +66 -0
- package/templates/default/src/pages/sitemap.xml.ts +77 -0
- package/templates/default/src/pages/submit.astro +416 -0
- package/templates/default/src/styles/global.css +84 -0
- package/templates/default/tailwind.config.mjs +130 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Container from "./layout/Container.astro";
|
|
3
|
+
|
|
4
|
+
const points = [
|
|
5
|
+
{
|
|
6
|
+
title: "Real apps",
|
|
7
|
+
body: "Not SDKs, templates, or tiny demos. Focused on complete applications you can actually run.",
|
|
8
|
+
icon: "app",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
title: "Codebase context",
|
|
12
|
+
body: "Stack, platform, license, activity, maturity, and contribution signals — one card per app.",
|
|
13
|
+
icon: "code",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: "Searchable discovery",
|
|
17
|
+
body: "Browse by category, stack, platform, stars, recency, and status — not a flat README list.",
|
|
18
|
+
icon: "search",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section id="about" class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800">
|
|
24
|
+
<Container>
|
|
25
|
+
<div class="flex flex-col gap-2">
|
|
26
|
+
<span
|
|
27
|
+
class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
28
|
+
>Why this exists</span
|
|
29
|
+
>
|
|
30
|
+
<h2
|
|
31
|
+
class="text-[1.5rem] sm:text-[1.75rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0 max-w-[40ch]"
|
|
32
|
+
>
|
|
33
|
+
Most open-source app discovery is fragmented.
|
|
34
|
+
</h2>
|
|
35
|
+
<p
|
|
36
|
+
class="text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
|
|
37
|
+
>
|
|
38
|
+
GitHub search shows repositories, not always usable products. Awesome
|
|
39
|
+
lists are helpful, but they tend to become long static README files.
|
|
40
|
+
This project focuses on real applications: projects with source code,
|
|
41
|
+
product context, stack information, activity signals, and enough
|
|
42
|
+
structure to help developers learn, compare, and contribute.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<ul
|
|
47
|
+
class="mt-7 grid list-none grid-cols-1 gap-3 p-0 m-0 sm:grid-cols-3"
|
|
48
|
+
>
|
|
49
|
+
{
|
|
50
|
+
points.map((p) => (
|
|
51
|
+
<li class="card p-4">
|
|
52
|
+
<div
|
|
53
|
+
class="mb-3 flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
{p.icon === "app" ? (
|
|
57
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
58
|
+
<path d="M3 2.75A2.25 2.25 0 0 1 5.25.5h5.5A2.25 2.25 0 0 1 13 2.75v10.5A2.25 2.25 0 0 1 10.75 15.5h-5.5A2.25 2.25 0 0 1 3 13.25V2.75ZM5.25 2a.75.75 0 0 0-.75.75v10.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75V2.75a.75.75 0 0 0-.75-.75h-5.5Z"/>
|
|
59
|
+
</svg>
|
|
60
|
+
) : p.icon === "code" ? (
|
|
61
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
62
|
+
<path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25Zm6.56 0a.75.75 0 1 1 1.06-1.06l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L13.94 8l-3.66-3.72Z"/>
|
|
63
|
+
</svg>
|
|
64
|
+
) : (
|
|
65
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
66
|
+
<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"/>
|
|
67
|
+
</svg>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
<h3 class="m-0 text-[0.9375rem] font-semibold text-ink-900 dark:text-ink-100">
|
|
71
|
+
{p.title}
|
|
72
|
+
</h3>
|
|
73
|
+
<p class="mt-1.5 m-0 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
74
|
+
{p.body}
|
|
75
|
+
</p>
|
|
76
|
+
</li>
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
</ul>
|
|
80
|
+
</Container>
|
|
81
|
+
</section>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Brand icon registry.
|
|
3
|
+
//
|
|
4
|
+
// Strategy: icons live as static SVGs in `public/icons/`. The fetch
|
|
5
|
+
// script (`scripts/fetch-icons.mjs`) downloads the well-known
|
|
6
|
+
// brand icons from the public `xandemon/developer-icons` repo on
|
|
7
|
+
// prebuild. Missing icons (windows, web, desktop, chromeos,
|
|
8
|
+
// embedded, ionic, capacitor) are hand-rolled in `public/icons/`.
|
|
9
|
+
//
|
|
10
|
+
// Theme-aware variants: some icons (Apple for iOS / macOS) ship
|
|
11
|
+
// in light and dark silhouettes and need to be swapped with
|
|
12
|
+
// the page's color scheme. The Icon component renders BOTH img
|
|
13
|
+
// tags and uses Tailwind's `dark:hidden` / `hidden dark:block` to
|
|
14
|
+
// show only the right one.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// <Icon name="Flutter" category="stack" size={12} />
|
|
18
|
+
// <Icon name="iOS" category="platform" size={12} />
|
|
19
|
+
//
|
|
20
|
+
// Falls back to a small generic dot if no icon is found so the chip
|
|
21
|
+
// still has visual weight.
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** Friendly name, e.g. "Flutter", "React Native", "iOS", "Web". */
|
|
25
|
+
name: string;
|
|
26
|
+
/** "stack" or "platform" — controls where we look in /icons/. */
|
|
27
|
+
category?: "stack" | "platform";
|
|
28
|
+
size?: number;
|
|
29
|
+
class?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { name, category = "stack", size = 12, class: className = "", title } = Astro.props;
|
|
34
|
+
|
|
35
|
+
function slugify(s: string): string {
|
|
36
|
+
return s
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/\+/g, "plus")
|
|
40
|
+
.replace(/\./g, "")
|
|
41
|
+
.replace(/[\s/]+/g, "-")
|
|
42
|
+
.replace(/-+/g, "-")
|
|
43
|
+
.replace(/^-|-$/g, "")
|
|
44
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Friendly name → icon filename. Add a new alias when a chip needs
|
|
48
|
+
// to render an icon that lives under a different filename.
|
|
49
|
+
const ALIASES: Record<string, string> = {
|
|
50
|
+
// stacks
|
|
51
|
+
react: "react",
|
|
52
|
+
"react-native": "react-native",
|
|
53
|
+
"node-js": "nodejs",
|
|
54
|
+
nodejs: "nodejs",
|
|
55
|
+
"node.js": "nodejs",
|
|
56
|
+
mongodb: "mongodb",
|
|
57
|
+
tensorflow: "tensorflow",
|
|
58
|
+
ionic: "ionic",
|
|
59
|
+
"ionic-capacitor": "ionic",
|
|
60
|
+
ionicframework: "ionic",
|
|
61
|
+
capacitor: "capacitor", // distinct from ionic — proper Capacitor brand mark
|
|
62
|
+
"kotlin-multiplatform": "kotlin",
|
|
63
|
+
kmp: "kotlin",
|
|
64
|
+
// platforms
|
|
65
|
+
ios: "ios",
|
|
66
|
+
ipados: "ios",
|
|
67
|
+
macos: "macos",
|
|
68
|
+
osx: "macos",
|
|
69
|
+
"mac-os": "macos",
|
|
70
|
+
web: "web",
|
|
71
|
+
chromeos: "chromeos",
|
|
72
|
+
"chrome-os": "chromeos",
|
|
73
|
+
windows: "windows",
|
|
74
|
+
win: "windows",
|
|
75
|
+
desktop: "desktop",
|
|
76
|
+
embedded: "embedded",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// These icons have explicit light + dark variants. We render both
|
|
80
|
+
// and let CSS swap based on the `.dark` class on <html>.
|
|
81
|
+
const THEME_AWARE = new Set([
|
|
82
|
+
"ios", // maps to apple-dark (light) + apple-light (dark)
|
|
83
|
+
"macos", // maps to apple-light (light) + apple-dark (dark)
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const slug = slugify(name);
|
|
87
|
+
const resolved = ALIASES[slug] ?? slug;
|
|
88
|
+
const baseDir = category === "platform" ? "platforms" : "stacks";
|
|
89
|
+
const url = (file: string) => `/icons/${baseDir}/${file}.svg`;
|
|
90
|
+
|
|
91
|
+
const isThemeAware = THEME_AWARE.has(resolved);
|
|
92
|
+
|
|
93
|
+
// Map theme-aware platform → { light: file, dark: file }
|
|
94
|
+
const THEME_PAIRS: Record<string, { light: string; dark: string }> = {
|
|
95
|
+
ios: { light: "apple-dark", dark: "apple-light" },
|
|
96
|
+
macos: { light: "apple-light", dark: "apple-dark" },
|
|
97
|
+
};
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
isThemeAware ? (
|
|
102
|
+
<>
|
|
103
|
+
{/* Light theme → light variant */}
|
|
104
|
+
<img
|
|
105
|
+
src={url(THEME_PAIRS[resolved].light)}
|
|
106
|
+
alt=""
|
|
107
|
+
width={size}
|
|
108
|
+
height={size}
|
|
109
|
+
loading="lazy"
|
|
110
|
+
decoding="async"
|
|
111
|
+
aria-hidden={title ? undefined : "true"}
|
|
112
|
+
class={`inline-block shrink-0 object-contain select-none dark:hidden ${className}`}
|
|
113
|
+
style={`width:${size}px;height:${size}px;`}
|
|
114
|
+
/>
|
|
115
|
+
{/* Dark theme → dark variant */}
|
|
116
|
+
<img
|
|
117
|
+
src={url(THEME_PAIRS[resolved].dark)}
|
|
118
|
+
alt=""
|
|
119
|
+
width={size}
|
|
120
|
+
height={size}
|
|
121
|
+
loading="lazy"
|
|
122
|
+
decoding="async"
|
|
123
|
+
aria-hidden={title ? undefined : "true"}
|
|
124
|
+
class={`inline-block shrink-0 object-contain select-none hidden dark:inline-block ${className}`}
|
|
125
|
+
style={`width:${size}px;height:${size}px;`}
|
|
126
|
+
/>
|
|
127
|
+
</>
|
|
128
|
+
) : resolved ? (
|
|
129
|
+
<img
|
|
130
|
+
src={url(resolved)}
|
|
131
|
+
alt=""
|
|
132
|
+
width={size}
|
|
133
|
+
height={size}
|
|
134
|
+
loading="lazy"
|
|
135
|
+
decoding="async"
|
|
136
|
+
aria-hidden={title ? undefined : "true"}
|
|
137
|
+
class={`inline-block shrink-0 object-contain select-none ${className}`}
|
|
138
|
+
style={`width:${size}px;height:${size}px;`}
|
|
139
|
+
/>
|
|
140
|
+
) : (
|
|
141
|
+
<span
|
|
142
|
+
aria-hidden="true"
|
|
143
|
+
class={`inline-block shrink-0 rounded-full bg-current opacity-40 ${className}`}
|
|
144
|
+
style={`width:${size}px;height:${size}px;`}
|
|
145
|
+
/>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
{title && <span class="sr-only">{title}</span>}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BaseLayout — wraps every page. Owns the theme-detection inline script
|
|
6
|
+
* and the global shell (Header, Footer). Per-page SEO (title, description,
|
|
7
|
+
* OG, JSON-LD) is delegated to <Seo /> so each page can declare what it
|
|
8
|
+
* needs without polluting the layout file.
|
|
9
|
+
*/
|
|
10
|
+
import "../../styles/global.css";
|
|
11
|
+
import Header from "./Header.astro";
|
|
12
|
+
import Footer from "./Footer.astro";
|
|
13
|
+
import Seo from "./Seo.astro";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
type?: "website" | "article" | "profile";
|
|
19
|
+
noindex?: boolean;
|
|
20
|
+
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
type,
|
|
27
|
+
noindex,
|
|
28
|
+
jsonLd,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const favicon =
|
|
32
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2318181b'/%3E%3C/svg%3E";
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<!doctype html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="UTF-8" />
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
40
|
+
<meta name="generator" content={Astro.generator} />
|
|
41
|
+
<meta name="theme-color" content="#0a0a0c" media="(prefers-color-scheme: dark)" />
|
|
42
|
+
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
|
43
|
+
<link rel="icon" href={favicon} />
|
|
44
|
+
<link rel="sitemap" href="/sitemap.xml" />
|
|
45
|
+
<link rel="alternate" type="text/plain" href="/llms.txt" title="LLM usage guidance" />
|
|
46
|
+
|
|
47
|
+
<Seo
|
|
48
|
+
title={title}
|
|
49
|
+
description={description}
|
|
50
|
+
type={type}
|
|
51
|
+
noindex={noindex}
|
|
52
|
+
jsonLd={jsonLd}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<slot name="head" />
|
|
56
|
+
<script is:inline>
|
|
57
|
+
(function () {
|
|
58
|
+
var STORAGE_KEY = "open-apps-theme";
|
|
59
|
+
function getMode() {
|
|
60
|
+
var stored = localStorage.getItem(STORAGE_KEY);
|
|
61
|
+
if (stored === "light" || stored === "dark" || stored === "system") return stored;
|
|
62
|
+
return "system";
|
|
63
|
+
}
|
|
64
|
+
function apply(mode) {
|
|
65
|
+
var resolved =
|
|
66
|
+
mode === "system"
|
|
67
|
+
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
|
68
|
+
: mode;
|
|
69
|
+
var root = document.documentElement;
|
|
70
|
+
if (resolved === "dark") root.classList.add("dark");
|
|
71
|
+
else root.classList.remove("dark");
|
|
72
|
+
root.dataset.theme = mode;
|
|
73
|
+
root.dataset.resolvedTheme = resolved;
|
|
74
|
+
}
|
|
75
|
+
apply(getMode());
|
|
76
|
+
window.addEventListener("theme:set", function (e) { apply(e.detail); });
|
|
77
|
+
var mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
78
|
+
mq.addEventListener("change", function () {
|
|
79
|
+
if (getMode() === "system") apply("system");
|
|
80
|
+
});
|
|
81
|
+
})();
|
|
82
|
+
</script>
|
|
83
|
+
</head>
|
|
84
|
+
<body class="min-h-screen bg-white text-ink-900 dark:bg-ink-950 dark:text-ink-100">
|
|
85
|
+
<Header />
|
|
86
|
+
|
|
87
|
+
<main>
|
|
88
|
+
<slot />
|
|
89
|
+
</main>
|
|
90
|
+
|
|
91
|
+
<Footer />
|
|
92
|
+
<slot name="body-end" />
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: "default" | "wide" | "narrow";
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { size = "default", class: className = "" } = Astro.props;
|
|
8
|
+
const sizeClass =
|
|
9
|
+
size === "wide"
|
|
10
|
+
? "max-w-wide"
|
|
11
|
+
: size === "narrow"
|
|
12
|
+
? "max-w-narrow"
|
|
13
|
+
: "max-w-container";
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div class={`${sizeClass} mx-auto px-5 sm:px-7 ${className}`}>
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { siteConfig } from "../../data/config";
|
|
3
|
+
import { stats } from "../../data/stats";
|
|
4
|
+
|
|
5
|
+
const githubUrl = stats.originalRepo;
|
|
6
|
+
const year = new Date().getFullYear();
|
|
7
|
+
|
|
8
|
+
// One-line positioning. Should be the same on every page so the
|
|
9
|
+
// project's identity is consistent.
|
|
10
|
+
const positioning =
|
|
11
|
+
"A searchable directory of real open-source applications — organized by stack, category, platform, license, activity, and maturity.";
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<footer class="mt-16 border-t border-ink-200 bg-white text-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-400">
|
|
15
|
+
<div
|
|
16
|
+
class="mx-auto grid max-w-wide grid-cols-2 gap-8 px-5 py-12 md:grid-cols-4 sm:px-7"
|
|
17
|
+
>
|
|
18
|
+
<div class="col-span-2 md:col-span-1">
|
|
19
|
+
<div
|
|
20
|
+
class="mb-2 inline-flex items-center gap-2 text-[0.95rem] font-semibold text-ink-900 dark:text-ink-100"
|
|
21
|
+
>
|
|
22
|
+
<span
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
class="inline-block h-3.5 w-3.5 rounded bg-ink-900 dark:bg-ink-100"
|
|
25
|
+
></span>
|
|
26
|
+
<span>{siteConfig.name}</span>
|
|
27
|
+
</div>
|
|
28
|
+
<p class="m-0 max-w-[36ch] text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
29
|
+
{positioning}
|
|
30
|
+
</p>
|
|
31
|
+
<p class="mt-3 m-0 text-2xs text-ink-500 dark:text-ink-400">
|
|
32
|
+
<span class="font-mono">{stats.apps}</span> apps
|
|
33
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
34
|
+
<span class="font-mono">{stats.categories}</span> categories
|
|
35
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
36
|
+
<span class="font-mono">{stats.contributors}</span> contributors
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<h4
|
|
42
|
+
class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100"
|
|
43
|
+
>
|
|
44
|
+
Discover
|
|
45
|
+
</h4>
|
|
46
|
+
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
47
|
+
<li><a href="#hot" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">Hot apps</a></li>
|
|
48
|
+
<li><a href="#new" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">New apps</a></li>
|
|
49
|
+
<li><a href="#mature" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">Mature apps</a></li>
|
|
50
|
+
<li><a href="#stacks" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">By stack</a></li>
|
|
51
|
+
<li><a href="#categories" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">By category</a></li>
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div>
|
|
56
|
+
<h4
|
|
57
|
+
class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100"
|
|
58
|
+
>
|
|
59
|
+
Contribute
|
|
60
|
+
</h4>
|
|
61
|
+
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
62
|
+
<li>
|
|
63
|
+
<a
|
|
64
|
+
href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
|
|
65
|
+
target="_blank"
|
|
66
|
+
rel="noopener noreferrer"
|
|
67
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
68
|
+
>Submit via GitHub</a
|
|
69
|
+
>
|
|
70
|
+
</li>
|
|
71
|
+
<li><a href="#contributors" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">Contributors</a></li>
|
|
72
|
+
<li>
|
|
73
|
+
<a
|
|
74
|
+
href={`${githubUrl}/issues`}
|
|
75
|
+
target="_blank"
|
|
76
|
+
rel="noopener noreferrer"
|
|
77
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
78
|
+
>Report an issue</a
|
|
79
|
+
>
|
|
80
|
+
</li>
|
|
81
|
+
<li>
|
|
82
|
+
<a
|
|
83
|
+
href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
87
|
+
>Contributing guide</a
|
|
88
|
+
>
|
|
89
|
+
</li>
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div>
|
|
94
|
+
<h4
|
|
95
|
+
class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100"
|
|
96
|
+
>
|
|
97
|
+
Project
|
|
98
|
+
</h4>
|
|
99
|
+
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
100
|
+
<li>
|
|
101
|
+
<a
|
|
102
|
+
href={githubUrl}
|
|
103
|
+
target="_blank"
|
|
104
|
+
rel="noopener noreferrer"
|
|
105
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
106
|
+
>Source on GitHub</a
|
|
107
|
+
>
|
|
108
|
+
</li>
|
|
109
|
+
<li><a href="#about" class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100">Why this exists</a></li>
|
|
110
|
+
<li>
|
|
111
|
+
<a
|
|
112
|
+
href={`${githubUrl}/blob/main/README.md`}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
116
|
+
>README</a
|
|
117
|
+
>
|
|
118
|
+
</li>
|
|
119
|
+
<li>
|
|
120
|
+
<a
|
|
121
|
+
href="/about"
|
|
122
|
+
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
123
|
+
>About</a
|
|
124
|
+
>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="border-t border-ink-200 dark:border-ink-800">
|
|
131
|
+
<div
|
|
132
|
+
class="mx-auto flex max-w-wide flex-wrap items-center gap-3 px-5 py-4 text-2xs text-ink-500 sm:px-7 dark:text-ink-400"
|
|
133
|
+
>
|
|
134
|
+
<span>© {year} {siteConfig.name}. Content under CC BY-SA 4.0.</span>
|
|
135
|
+
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
136
|
+
<a
|
|
137
|
+
href={githubUrl}
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
class="transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
141
|
+
>View source on GitHub</a
|
|
142
|
+
>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</footer>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { siteConfig } from "../../data/config";
|
|
3
|
+
import ThemeToggle from "./ThemeToggle.astro";
|
|
4
|
+
import { stats } from "../../data/stats";
|
|
5
|
+
import { compact } from "../../lib/format";
|
|
6
|
+
|
|
7
|
+
const githubUrl = stats.originalRepo;
|
|
8
|
+
|
|
9
|
+
// Nav items. Order is information-architecture: discover → browse → act.
|
|
10
|
+
// `Contributors` is intentionally not a top-level item — it lives lower
|
|
11
|
+
// on the page. The watchlist was removed in v0.1 (see CHANGELOG).
|
|
12
|
+
//
|
|
13
|
+
// All links point to /apps with a filter rather than in-page anchors.
|
|
14
|
+
// This keeps the nav usable from any page (e.g. /about) — anchor links
|
|
15
|
+
// would only work on the home page, leaving /apps and /about with dead
|
|
16
|
+
// links. Category/Stack filters are exposed via the /apps refine panel
|
|
17
|
+
// and the home page's "Browse by stack"/"Browse by category" sections.
|
|
18
|
+
const navItems = [
|
|
19
|
+
{ label: "New", href: "/apps?label=new" },
|
|
20
|
+
{ label: "Hot", href: "/apps?label=hot" },
|
|
21
|
+
{ label: "Categories", href: "/apps" },
|
|
22
|
+
{ label: "Stacks", href: "/apps" },
|
|
23
|
+
{ label: "About", href: "/about" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const starsLabel = compact(stats.stars);
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<header
|
|
30
|
+
class="sticky top-0 z-50 border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85"
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
class="mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
|
|
34
|
+
>
|
|
35
|
+
<a
|
|
36
|
+
href="/"
|
|
37
|
+
class="inline-flex items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
38
|
+
>
|
|
39
|
+
<span
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
|
|
42
|
+
></span>
|
|
43
|
+
<span>{siteConfig.name}</span>
|
|
44
|
+
</a>
|
|
45
|
+
|
|
46
|
+
<nav class="hidden flex-1 md:block" aria-label="Primary">
|
|
47
|
+
<ul class="m-0 flex list-none gap-5 p-0">
|
|
48
|
+
{
|
|
49
|
+
navItems.map((item) => (
|
|
50
|
+
<li>
|
|
51
|
+
<a
|
|
52
|
+
href={item.href}
|
|
53
|
+
class="text-[0.875rem] font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
54
|
+
>
|
|
55
|
+
{item.label}
|
|
56
|
+
</a>
|
|
57
|
+
</li>
|
|
58
|
+
))
|
|
59
|
+
}
|
|
60
|
+
</ul>
|
|
61
|
+
</nav>
|
|
62
|
+
|
|
63
|
+
<div class="ml-auto flex items-center gap-2">
|
|
64
|
+
<a
|
|
65
|
+
href="/submit"
|
|
66
|
+
class="hidden h-9 items-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 sm:inline-flex dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
|
|
67
|
+
>
|
|
68
|
+
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
|
|
69
|
+
<path d="M7.75 2a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 7.75 2Z"/>
|
|
70
|
+
</svg>
|
|
71
|
+
<span>Submit app</span>
|
|
72
|
+
</a>
|
|
73
|
+
|
|
74
|
+
{/* GitHub button. The star count is rendered as text, not an
|
|
75
|
+
external badge image, so it works offline, doesn't depend
|
|
76
|
+
on shields.io, and reflects what we already know (stats.stars). */}
|
|
77
|
+
<a
|
|
78
|
+
href={githubUrl}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
aria-label={`View source on GitHub — ${stats.stars} stars`}
|
|
82
|
+
title="View source on GitHub"
|
|
83
|
+
class="inline-flex h-9 items-center divide-x divide-ink-200 overflow-hidden rounded-md border border-ink-200 text-[0.875rem] font-medium transition-colors hover:border-ink-300 hover:bg-ink-50 dark:divide-ink-800 dark:border-ink-800 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
|
|
84
|
+
>
|
|
85
|
+
<span class="flex items-center gap-1.5 px-2.5 py-1.5 text-ink-900 dark:text-ink-100">
|
|
86
|
+
<svg
|
|
87
|
+
viewBox="0 0 16 16"
|
|
88
|
+
width="16"
|
|
89
|
+
height="16"
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
fill="currentColor"
|
|
92
|
+
>
|
|
93
|
+
<path
|
|
94
|
+
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"
|
|
95
|
+
/>
|
|
96
|
+
</svg>
|
|
97
|
+
<span>Star</span>
|
|
98
|
+
</span>
|
|
99
|
+
<span class="flex items-center gap-1 bg-ink-50 px-2 py-1.5 text-ink-700 dark:bg-ink-900/60 dark:text-ink-300">
|
|
100
|
+
<svg
|
|
101
|
+
viewBox="0 0 16 16"
|
|
102
|
+
width="11"
|
|
103
|
+
height="11"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
fill="currentColor"
|
|
106
|
+
class="text-amber-500"
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
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"
|
|
110
|
+
/>
|
|
111
|
+
</svg>
|
|
112
|
+
<span class="font-mono">{starsLabel}</span>
|
|
113
|
+
</span>
|
|
114
|
+
</a>
|
|
115
|
+
<ThemeToggle />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</header>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
// Smooth-scroll for in-page anchor links (Header nav + Hero CTA).
|
|
122
|
+
// Works on any <a href="#…"> that points to an existing element on
|
|
123
|
+
// the current page. External links (e.g. /about, /apps?…) are skipped
|
|
124
|
+
// because they don't start with "#". Modifier-clicks (Cmd/Ctrl/Shift/
|
|
125
|
+
// middle) let the browser handle the anchor normally so users can
|
|
126
|
+
// open the same section in a new tab.
|
|
127
|
+
document.addEventListener("click", (e) => {
|
|
128
|
+
if (e.defaultPrevented) return;
|
|
129
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
130
|
+
if (e.button !== 0) return; // non-primary mouse button
|
|
131
|
+
const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
|
|
132
|
+
'a[href^="#"]',
|
|
133
|
+
);
|
|
134
|
+
if (!link) return;
|
|
135
|
+
const id = link.getAttribute("href")!;
|
|
136
|
+
if (id.length <= 1) return; // bare "#" → ignore
|
|
137
|
+
const target = document.querySelector(id);
|
|
138
|
+
if (!target) return; // not on this page; let the browser navigate
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
141
|
+
// Update the URL hash without an extra jump, so back/forward works.
|
|
142
|
+
history.replaceState(null, "", id);
|
|
143
|
+
});
|
|
144
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
eyebrow?: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
align?: "left" | "center";
|
|
7
|
+
level?: 2 | 3;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { eyebrow, title, description, align = "left", level = 2 } = Astro.props;
|
|
11
|
+
const Heading = `h${level}` as "h2" | "h3";
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<header
|
|
15
|
+
class={`mb-6 flex flex-col gap-2 ${
|
|
16
|
+
align === "center" ? "items-center text-center" : ""
|
|
17
|
+
}`}
|
|
18
|
+
>
|
|
19
|
+
{
|
|
20
|
+
eyebrow && (
|
|
21
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
22
|
+
{eyebrow}
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
<Heading
|
|
27
|
+
class="text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
|
|
28
|
+
>
|
|
29
|
+
{title}
|
|
30
|
+
</Heading>
|
|
31
|
+
{
|
|
32
|
+
description && (
|
|
33
|
+
<p
|
|
34
|
+
class={`text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 ${
|
|
35
|
+
align === "center" ? "max-w-2xl" : "max-w-[56ch]"
|
|
36
|
+
}`}
|
|
37
|
+
>
|
|
38
|
+
{description}
|
|
39
|
+
</p>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
</header>
|