@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.
Files changed (109) hide show
  1. package/README.md +81 -0
  2. package/dist/index.d.ts +26 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +26 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +58 -0
  7. package/src/components/ActiveFilterChips.astro +20 -0
  8. package/src/components/CategoryGrid.astro +27 -0
  9. package/src/components/DirectoryFilters.astro +100 -0
  10. package/src/components/DirectoryHero.astro +32 -0
  11. package/src/components/HealthBadge.astro +12 -0
  12. package/src/components/ItemCard.astro +41 -0
  13. package/src/components/LensTabs.astro +26 -0
  14. package/src/components/MethodologyPanel.astro +20 -0
  15. package/src/components/Pagination.astro +24 -0
  16. package/src/components/ProjectDetail.astro +166 -0
  17. package/src/components/ScoreBars.astro +32 -0
  18. package/src/components/SubmitDraft.astro +137 -0
  19. package/src/index.ts +17 -0
  20. package/src/layouts/BaseLayout.astro +32 -0
  21. package/src/styles.css +675 -0
  22. package/templates/default/astro.config.mjs +23 -0
  23. package/templates/default/data/generated/apps.json +1 -0
  24. package/templates/default/data/generated/repo-stats.json +1 -0
  25. package/templates/default/package.json +35 -0
  26. package/templates/default/public/icons/platforms/android.svg +1 -0
  27. package/templates/default/public/icons/platforms/apple-dark.svg +1 -0
  28. package/templates/default/public/icons/platforms/apple-light.svg +1 -0
  29. package/templates/default/public/icons/platforms/chrome.svg +1 -0
  30. package/templates/default/public/icons/platforms/chromeos.svg +5 -0
  31. package/templates/default/public/icons/platforms/desktop.svg +4 -0
  32. package/templates/default/public/icons/platforms/embedded.svg +5 -0
  33. package/templates/default/public/icons/platforms/ios.svg +1 -0
  34. package/templates/default/public/icons/platforms/linux.svg +1 -0
  35. package/templates/default/public/icons/platforms/macos.svg +1 -0
  36. package/templates/default/public/icons/platforms/ubuntu.svg +1 -0
  37. package/templates/default/public/icons/platforms/web.svg +5 -0
  38. package/templates/default/public/icons/platforms/windows.svg +6 -0
  39. package/templates/default/public/icons/stacks/android.svg +1 -0
  40. package/templates/default/public/icons/stacks/apple-dark.svg +1 -0
  41. package/templates/default/public/icons/stacks/apple-light.svg +1 -0
  42. package/templates/default/public/icons/stacks/capacitor.svg +14 -0
  43. package/templates/default/public/icons/stacks/dart.svg +1 -0
  44. package/templates/default/public/icons/stacks/firebase.svg +1 -0
  45. package/templates/default/public/icons/stacks/flutter.svg +1 -0
  46. package/templates/default/public/icons/stacks/graphql.svg +1 -0
  47. package/templates/default/public/icons/stacks/ionic.svg +4 -0
  48. package/templates/default/public/icons/stacks/java.svg +1 -0
  49. package/templates/default/public/icons/stacks/javascript.svg +1 -0
  50. package/templates/default/public/icons/stacks/kotlin.svg +1 -0
  51. package/templates/default/public/icons/stacks/mongodb.svg +1 -0
  52. package/templates/default/public/icons/stacks/nodejs.svg +1 -0
  53. package/templates/default/public/icons/stacks/python.svg +1 -0
  54. package/templates/default/public/icons/stacks/react-native.svg +1 -0
  55. package/templates/default/public/icons/stacks/react.svg +1 -0
  56. package/templates/default/public/icons/stacks/rust.svg +1 -0
  57. package/templates/default/public/icons/stacks/solidity.svg +1 -0
  58. package/templates/default/public/icons/stacks/swift.svg +1 -0
  59. package/templates/default/public/icons/stacks/tensorflow.svg +1 -0
  60. package/templates/default/public/icons/stacks/typescript.svg +1 -0
  61. package/templates/default/public/og-image.svg +48 -0
  62. package/templates/default/public/robots.txt +53 -0
  63. package/templates/default/src/components/AppCard.astro +248 -0
  64. package/templates/default/src/components/AppSection.astro +58 -0
  65. package/templates/default/src/components/AppsIndexRow.astro +230 -0
  66. package/templates/default/src/components/AppsPagination.astro +147 -0
  67. package/templates/default/src/components/CategoryGrid.astro +80 -0
  68. package/templates/default/src/components/ContributorsGrid.astro +105 -0
  69. package/templates/default/src/components/DecisionRow.astro +219 -0
  70. package/templates/default/src/components/ExploreByCategory.astro +74 -0
  71. package/templates/default/src/components/ExploreByStack.astro +102 -0
  72. package/templates/default/src/components/Hero.astro +176 -0
  73. package/templates/default/src/components/MinimalAbout.astro +57 -0
  74. package/templates/default/src/components/OriginalCollection.astro +144 -0
  75. package/templates/default/src/components/RefinePanel.astro +272 -0
  76. package/templates/default/src/components/ScoreBars.astro +72 -0
  77. package/templates/default/src/components/SmartLensTabs.astro +65 -0
  78. package/templates/default/src/components/StackGrid.astro +95 -0
  79. package/templates/default/src/components/WhyThisExists.astro +81 -0
  80. package/templates/default/src/components/icons/Icon.astro +148 -0
  81. package/templates/default/src/components/layout/BaseLayout.astro +94 -0
  82. package/templates/default/src/components/layout/Container.astro +18 -0
  83. package/templates/default/src/components/layout/Footer.astro +145 -0
  84. package/templates/default/src/components/layout/Header.astro +144 -0
  85. package/templates/default/src/components/layout/SectionHeader.astro +42 -0
  86. package/templates/default/src/components/layout/Seo.astro +120 -0
  87. package/templates/default/src/components/layout/ThemeToggle.astro +97 -0
  88. package/templates/default/src/data/apps.ts +149 -0
  89. package/templates/default/src/data/categories.ts +27 -0
  90. package/templates/default/src/data/config.ts +28 -0
  91. package/templates/default/src/data/contributors.ts +96 -0
  92. package/templates/default/src/data/stacks.ts +47 -0
  93. package/templates/default/src/data/stats.ts +65 -0
  94. package/templates/default/src/data/types.ts +248 -0
  95. package/templates/default/src/lib/apps-search.ts +228 -0
  96. package/templates/default/src/lib/format.ts +36 -0
  97. package/templates/default/src/lib/lenses.ts +98 -0
  98. package/templates/default/src/lib/repo.ts +12 -0
  99. package/templates/default/src/lib/scores.ts +51 -0
  100. package/templates/default/src/lib/taxonomy-counts.ts +31 -0
  101. package/templates/default/src/pages/about.astro +433 -0
  102. package/templates/default/src/pages/apps/[slug].astro +709 -0
  103. package/templates/default/src/pages/apps/index.astro +669 -0
  104. package/templates/default/src/pages/contributors.astro +142 -0
  105. package/templates/default/src/pages/index.astro +66 -0
  106. package/templates/default/src/pages/sitemap.xml.ts +77 -0
  107. package/templates/default/src/pages/submit.astro +416 -0
  108. package/templates/default/src/styles/global.css +84 -0
  109. 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>