@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,416 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { siteConfig } from "../data/config";
|
|
3
|
+
import BaseLayout from "../components/layout/BaseLayout.astro";
|
|
4
|
+
import Container from "../components/layout/Container.astro";
|
|
5
|
+
import { stacks } from "../data/stacks";
|
|
6
|
+
import { categories } from "../data/categories";
|
|
7
|
+
|
|
8
|
+
const platformOptions = [
|
|
9
|
+
{ id: "ios", name: "iOS" },
|
|
10
|
+
{ id: "android", name: "Android" },
|
|
11
|
+
{ id: "web", name: "Web" },
|
|
12
|
+
{ id: "macos", name: "macOS" },
|
|
13
|
+
{ id: "windows", name: "Windows" },
|
|
14
|
+
{ id: "linux", name: "Linux" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const distributionOptions = [
|
|
18
|
+
{ type: "app-store", label: "App Store", platform: "ios", placeholder: "https://apps.apple.com/..." },
|
|
19
|
+
{ type: "play-store", label: "Play Store", platform: "android", placeholder: "https://play.google.com/store/apps/details?id=..." },
|
|
20
|
+
{ type: "fdroid", label: "F-Droid", platform: "android", placeholder: "https://f-droid.org/packages/..." },
|
|
21
|
+
{ type: "github-releases", label: "GitHub Releases", platform: "android", placeholder: "https://github.com/owner/repo/releases" },
|
|
22
|
+
{ type: "apk", label: "APK download", platform: "android", placeholder: "https://example.com/app.apk" },
|
|
23
|
+
{ type: "testflight", label: "TestFlight", platform: "ios", placeholder: "https://testflight.apple.com/join/..." },
|
|
24
|
+
{ type: "website", label: "Website download", platform: "web", placeholder: "https://example.com/download" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const title = "Submit App - ${siteConfig.name}";
|
|
28
|
+
const description = "Submit an open-source app to ${siteConfig.name}.";
|
|
29
|
+
// The submit page is a thin wrapper around a GitHub Issues form —
|
|
30
|
+
// no content of its own. Keep it out of search results and out of
|
|
31
|
+
// the sitemap.
|
|
32
|
+
const noindex = true;
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<BaseLayout title={title} description={description} noindex={noindex}>
|
|
36
|
+
<Container>
|
|
37
|
+
<section class="py-10 sm:py-14">
|
|
38
|
+
<div class="max-w-3xl">
|
|
39
|
+
<p class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
40
|
+
App submission
|
|
41
|
+
</p>
|
|
42
|
+
<h1 class="mt-2 text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
43
|
+
Add an app from GitHub
|
|
44
|
+
</h1>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,0.8fr)]">
|
|
48
|
+
<form id="submit-form" class="card p-5">
|
|
49
|
+
<div class="grid gap-5">
|
|
50
|
+
<label class="grid gap-1.5">
|
|
51
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">GitHub URL</span>
|
|
52
|
+
<div class="flex flex-col gap-2 sm:flex-row">
|
|
53
|
+
<input
|
|
54
|
+
id="repo-url"
|
|
55
|
+
name="repoUrl"
|
|
56
|
+
type="url"
|
|
57
|
+
required
|
|
58
|
+
placeholder="https://github.com/owner/repo"
|
|
59
|
+
class="min-h-10 flex-1 rounded-md border border-ink-200 bg-white px-3 text-sm text-ink-900 outline-none focus:border-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
|
|
60
|
+
/>
|
|
61
|
+
<button id="fetch-repo" type="button" class="btn-primary min-h-10 justify-center">
|
|
62
|
+
Fetch draft
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</label>
|
|
66
|
+
|
|
67
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
68
|
+
<label class="grid gap-1.5">
|
|
69
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Name</span>
|
|
70
|
+
<input id="app-name" type="text" class="field" />
|
|
71
|
+
</label>
|
|
72
|
+
<label class="grid gap-1.5">
|
|
73
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Slug</span>
|
|
74
|
+
<input id="app-slug" type="text" class="field font-mono" />
|
|
75
|
+
</label>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<label class="grid gap-1.5">
|
|
79
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Description</span>
|
|
80
|
+
<textarea id="app-description" rows="3" class="field resize-y"></textarea>
|
|
81
|
+
</label>
|
|
82
|
+
|
|
83
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
84
|
+
<label class="grid gap-1.5">
|
|
85
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Category</span>
|
|
86
|
+
<select id="app-category" class="field">
|
|
87
|
+
{categories.map((category) => (
|
|
88
|
+
<option value={category.slug}>{category.name}</option>
|
|
89
|
+
))}
|
|
90
|
+
</select>
|
|
91
|
+
</label>
|
|
92
|
+
<label class="grid gap-1.5">
|
|
93
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Primary stack</span>
|
|
94
|
+
<select id="primary-stack" class="field">
|
|
95
|
+
{stacks.map((stack) => (
|
|
96
|
+
<option value={stack.slug}>{stack.name}</option>
|
|
97
|
+
))}
|
|
98
|
+
</select>
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<fieldset class="grid gap-2">
|
|
103
|
+
<legend class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
104
|
+
Platforms
|
|
105
|
+
</legend>
|
|
106
|
+
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
107
|
+
{platformOptions.map((platform) => (
|
|
108
|
+
<label class="flex min-h-9 items-center gap-2 rounded-md border border-ink-200 px-2 text-sm dark:border-ink-800">
|
|
109
|
+
<input type="checkbox" name="platform" value={platform.id} class="h-4 w-4" />
|
|
110
|
+
<span>{platform.name}</span>
|
|
111
|
+
</label>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</fieldset>
|
|
115
|
+
|
|
116
|
+
<label class="grid gap-1.5">
|
|
117
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Tags</span>
|
|
118
|
+
<input id="app-tags" type="text" placeholder="self-hosted, photos, offline-first" class="field" />
|
|
119
|
+
</label>
|
|
120
|
+
|
|
121
|
+
<fieldset class="grid gap-2">
|
|
122
|
+
<legend class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
123
|
+
Download links
|
|
124
|
+
</legend>
|
|
125
|
+
<div class="grid gap-2">
|
|
126
|
+
{distributionOptions.map((option) => (
|
|
127
|
+
<label class="grid gap-1 rounded-md border border-ink-200 p-2 dark:border-ink-800">
|
|
128
|
+
<span class="text-xs font-medium text-ink-700 dark:text-ink-200">{option.label}</span>
|
|
129
|
+
<input
|
|
130
|
+
type="url"
|
|
131
|
+
data-distribution-type={option.type}
|
|
132
|
+
data-distribution-label={option.label}
|
|
133
|
+
data-distribution-platform={option.platform}
|
|
134
|
+
placeholder={option.placeholder}
|
|
135
|
+
class="field min-h-9 text-xs"
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</fieldset>
|
|
141
|
+
|
|
142
|
+
<label class="grid gap-1.5">
|
|
143
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Best for</span>
|
|
144
|
+
<textarea id="best-for" rows="3" class="field resize-y"></textarea>
|
|
145
|
+
</label>
|
|
146
|
+
</div>
|
|
147
|
+
</form>
|
|
148
|
+
|
|
149
|
+
<aside class="card flex min-h-[520px] flex-col p-5">
|
|
150
|
+
<div class="flex items-center justify-between gap-3">
|
|
151
|
+
<h2 class="m-0 text-base font-semibold text-ink-900 dark:text-ink-100">YAML draft</h2>
|
|
152
|
+
<a id="open-pr-link" href="#" target="_blank" rel="noopener noreferrer" class="btn-outline pointer-events-none opacity-50">
|
|
153
|
+
Open PR draft
|
|
154
|
+
</a>
|
|
155
|
+
</div>
|
|
156
|
+
<pre class="mt-4 min-h-0 flex-1 overflow-auto rounded-md border border-ink-200 bg-ink-50 p-3 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code id="yaml-preview">Paste a GitHub URL to generate a draft.</code></pre>
|
|
157
|
+
<p id="submit-status" class="mt-3 min-h-5 text-2xs text-ink-500 dark:text-ink-400" aria-live="polite" aria-atomic="true"></p>
|
|
158
|
+
</aside>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
</Container>
|
|
162
|
+
|
|
163
|
+
<style>
|
|
164
|
+
@reference "../styles/global.css";
|
|
165
|
+
|
|
166
|
+
.field {
|
|
167
|
+
@apply min-h-10 rounded-md border border-ink-200 bg-white px-3 py-2 text-sm text-ink-900 outline-none focus:border-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
170
|
+
|
|
171
|
+
<script>
|
|
172
|
+
// @ts-nocheck
|
|
173
|
+
const repoUrl = document.querySelector("#repo-url");
|
|
174
|
+
const fetchButton = document.querySelector("#fetch-repo");
|
|
175
|
+
const status = document.querySelector("#submit-status");
|
|
176
|
+
const preview = document.querySelector("#yaml-preview");
|
|
177
|
+
const openPrLink = document.querySelector("#open-pr-link");
|
|
178
|
+
const fields = {
|
|
179
|
+
name: document.querySelector("#app-name"),
|
|
180
|
+
slug: document.querySelector("#app-slug"),
|
|
181
|
+
description: document.querySelector("#app-description"),
|
|
182
|
+
category: document.querySelector("#app-category"),
|
|
183
|
+
primaryStack: document.querySelector("#primary-stack"),
|
|
184
|
+
tags: document.querySelector("#app-tags"),
|
|
185
|
+
bestFor: document.querySelector("#best-for"),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let repository = null;
|
|
189
|
+
|
|
190
|
+
function slugify(value) {
|
|
191
|
+
return String(value || "")
|
|
192
|
+
.toLowerCase()
|
|
193
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
194
|
+
.replace(/^-+|-+$/g, "");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseGithubUrl(value) {
|
|
198
|
+
const match = String(value).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
199
|
+
if (!match) return null;
|
|
200
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function inferStack(repo) {
|
|
204
|
+
const language = String(repo.language || "").toLowerCase();
|
|
205
|
+
const topics = (repo.topics || []).map((topic) => String(topic).toLowerCase());
|
|
206
|
+
if (topics.includes("flutter") || language === "dart") return "flutter";
|
|
207
|
+
if (topics.includes("react-native")) return "react-native";
|
|
208
|
+
if (topics.includes("capacitor") || topics.includes("ionic")) return "capacitor";
|
|
209
|
+
if (language === "swift" || language === "objective-c") return "ios";
|
|
210
|
+
if (language === "kotlin" || language === "java") return "android";
|
|
211
|
+
// Unknown — return empty so the user has to pick. The previous
|
|
212
|
+
// default of "flutter" mis-tagged Java/Kotlin/Swift-only apps.
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function inferPlatforms(repo, stack) {
|
|
217
|
+
const topics = (repo.topics || []).map((topic) => String(topic).toLowerCase());
|
|
218
|
+
const values = new Set();
|
|
219
|
+
for (const topic of topics) {
|
|
220
|
+
if (["ios", "android", "web", "macos", "windows", "linux"].includes(topic)) values.add(topic);
|
|
221
|
+
}
|
|
222
|
+
if (stack === "ios") values.add("ios");
|
|
223
|
+
if (stack === "android") values.add("android");
|
|
224
|
+
if (stack === "flutter" || stack === "react-native" || stack === "capacitor") {
|
|
225
|
+
values.add("ios");
|
|
226
|
+
values.add("android");
|
|
227
|
+
}
|
|
228
|
+
return [...values];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function setPlatforms(values) {
|
|
232
|
+
const selected = new Set(values);
|
|
233
|
+
document.querySelectorAll("input[name='platform']").forEach((input) => {
|
|
234
|
+
input.checked = selected.has(input.value);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function selectedPlatforms() {
|
|
239
|
+
return [...document.querySelectorAll("input[name='platform']:checked")].map((input) => input.value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function distributionChannels() {
|
|
243
|
+
return [...document.querySelectorAll("[data-distribution-type]")]
|
|
244
|
+
.map((input) => ({
|
|
245
|
+
type: input.dataset.distributionType,
|
|
246
|
+
label: input.dataset.distributionLabel,
|
|
247
|
+
platform: input.dataset.distributionPlatform,
|
|
248
|
+
url: input.value.trim(),
|
|
249
|
+
}))
|
|
250
|
+
.filter((channel) => channel.url);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function listLines(values, indent) {
|
|
254
|
+
return values.map((value) => `${" ".repeat(indent)}- ${value}`).join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function yamlQuote(value) {
|
|
258
|
+
const safe = String(value ?? "").replace(/"/g, '\\"');
|
|
259
|
+
return `"${safe}"`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function generateYaml() {
|
|
263
|
+
if (!repository) return "";
|
|
264
|
+
const parsed = parseGithubUrl(repoUrl.value);
|
|
265
|
+
if (!parsed) return "";
|
|
266
|
+
const slug = slugify(fields.slug.value || repository.name);
|
|
267
|
+
const tags = fields.tags.value.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
268
|
+
const bestFor = fields.bestFor.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
269
|
+
const platforms = selectedPlatforms();
|
|
270
|
+
const distribution = distributionChannels();
|
|
271
|
+
const stack = fields.primaryStack.value;
|
|
272
|
+
const topics = repository.topics || [];
|
|
273
|
+
const technologies = [...new Set([stack, String(repository.language || "").toLowerCase(), ...topics])].filter(Boolean);
|
|
274
|
+
const now = new Date().toISOString();
|
|
275
|
+
|
|
276
|
+
return [
|
|
277
|
+
"schemaVersion: 1",
|
|
278
|
+
"",
|
|
279
|
+
`id: github:${repository.full_name}`,
|
|
280
|
+
`slug: ${slug}`,
|
|
281
|
+
"",
|
|
282
|
+
"source:",
|
|
283
|
+
" provider: github",
|
|
284
|
+
` owner: ${parsed.owner}`,
|
|
285
|
+
` repo: ${parsed.repo}`,
|
|
286
|
+
` url: ${repository.html_url}`,
|
|
287
|
+
"",
|
|
288
|
+
"app:",
|
|
289
|
+
` name: ${yamlQuote(fields.name.value)}`,
|
|
290
|
+
` description: ${yamlQuote(fields.description.value)}`,
|
|
291
|
+
` category: ${fields.category.value}`,
|
|
292
|
+
" projectType: production",
|
|
293
|
+
" platforms:",
|
|
294
|
+
platforms.length ? listLines(platforms, 4) : " - ios",
|
|
295
|
+
" tags:",
|
|
296
|
+
tags.length ? listLines(tags, 4) : listLines(topics.slice(0, 5), 4),
|
|
297
|
+
...(distribution.length
|
|
298
|
+
? [
|
|
299
|
+
" distribution:",
|
|
300
|
+
" channels:",
|
|
301
|
+
...distribution.map((channel) => [
|
|
302
|
+
` - type: ${channel.type}`,
|
|
303
|
+
` platform: ${channel.platform}`,
|
|
304
|
+
` label: ${yamlQuote(channel.label)}`,
|
|
305
|
+
` url: ${channel.url}`,
|
|
306
|
+
" verified: false",
|
|
307
|
+
].join("\n")),
|
|
308
|
+
]
|
|
309
|
+
: []),
|
|
310
|
+
"",
|
|
311
|
+
"stack:",
|
|
312
|
+
` primary: ${stack}`,
|
|
313
|
+
" technologies:",
|
|
314
|
+
...technologies.slice(0, 10).map((id) => ` - id: ${id}\n role: technology`),
|
|
315
|
+
"",
|
|
316
|
+
"github:",
|
|
317
|
+
" repository:",
|
|
318
|
+
` id: ${repository.id}`,
|
|
319
|
+
` node_id: ${repository.node_id}`,
|
|
320
|
+
` name: ${repository.name}`,
|
|
321
|
+
` full_name: ${repository.full_name}`,
|
|
322
|
+
` html_url: ${repository.html_url}`,
|
|
323
|
+
` clone_url: ${repository.clone_url}`,
|
|
324
|
+
` ssh_url: ${repository.ssh_url}`,
|
|
325
|
+
` homepage: ${repository.homepage ? yamlQuote(repository.homepage) : "null"}`,
|
|
326
|
+
` description: ${repository.description ? yamlQuote(repository.description) : "null"}`,
|
|
327
|
+
` fork: ${Boolean(repository.fork)}`,
|
|
328
|
+
` archived: ${Boolean(repository.archived)}`,
|
|
329
|
+
` disabled: ${Boolean(repository.disabled)}`,
|
|
330
|
+
` private: ${Boolean(repository.private)}`,
|
|
331
|
+
` is_template: ${Boolean(repository.is_template)}`,
|
|
332
|
+
` visibility: ${repository.visibility || "public"}`,
|
|
333
|
+
` default_branch: ${repository.default_branch}`,
|
|
334
|
+
` language: ${repository.language ? yamlQuote(repository.language) : "null"}`,
|
|
335
|
+
" topics:",
|
|
336
|
+
topics.length ? listLines(topics, 6) : " []",
|
|
337
|
+
" license:",
|
|
338
|
+
` key: ${repository.license?.key || "unknown"}`,
|
|
339
|
+
` name: ${repository.license?.name ? yamlQuote(repository.license.name) : "null"}`,
|
|
340
|
+
` spdx_id: ${repository.license?.spdx_id || "NOASSERTION"}`,
|
|
341
|
+
` stargazers_count: ${repository.stargazers_count || 0}`,
|
|
342
|
+
` watchers_count: ${repository.watchers_count || 0}`,
|
|
343
|
+
` forks_count: ${repository.forks_count || 0}`,
|
|
344
|
+
` open_issues_count: ${repository.open_issues_count || 0}`,
|
|
345
|
+
` subscribers_count: ${repository.subscribers_count || 0}`,
|
|
346
|
+
` size: ${repository.size || 0}`,
|
|
347
|
+
` created_at: ${repository.created_at}`,
|
|
348
|
+
` updated_at: ${repository.updated_at}`,
|
|
349
|
+
` pushed_at: ${repository.pushed_at}`,
|
|
350
|
+
" sync:",
|
|
351
|
+
` syncedAt: ${now}`,
|
|
352
|
+
" apiVersion: rest-v3",
|
|
353
|
+
" source: submit-page",
|
|
354
|
+
"",
|
|
355
|
+
"curation:",
|
|
356
|
+
" reviewed: false",
|
|
357
|
+
" reviewedBy: null",
|
|
358
|
+
" reviewedAt: null",
|
|
359
|
+
" bestFor:",
|
|
360
|
+
bestFor.length ? listLines(bestFor, 4) : " []",
|
|
361
|
+
" caveats:",
|
|
362
|
+
" []",
|
|
363
|
+
"",
|
|
364
|
+
].join("\n");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function refreshYaml() {
|
|
368
|
+
const yaml = generateYaml();
|
|
369
|
+
preview.textContent = yaml || "Paste a GitHub URL to generate a draft.";
|
|
370
|
+
if (!yaml || !repository) {
|
|
371
|
+
openPrLink.href = "#";
|
|
372
|
+
openPrLink.classList.add("pointer-events-none", "opacity-50");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const slug = slugify(fields.slug.value || repository.name);
|
|
376
|
+
const params = new URLSearchParams({
|
|
377
|
+
filename: `data/apps/${slug}.yml`,
|
|
378
|
+
value: yaml,
|
|
379
|
+
});
|
|
380
|
+
openPrLink.href = `https://github.com/tortuvshin/open-apps/new/main?${params.toString()}`;
|
|
381
|
+
openPrLink.classList.remove("pointer-events-none", "opacity-50");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fetchButton.addEventListener("click", async () => {
|
|
385
|
+
const parsed = parseGithubUrl(repoUrl.value);
|
|
386
|
+
if (!parsed) {
|
|
387
|
+
status.textContent = "Enter a valid GitHub repository URL.";
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
fetchButton.disabled = true;
|
|
391
|
+
status.textContent = "Fetching GitHub metadata...";
|
|
392
|
+
try {
|
|
393
|
+
const response = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, {
|
|
394
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
395
|
+
});
|
|
396
|
+
if (!response.ok) throw new Error(`GitHub returned ${response.status}`);
|
|
397
|
+
repository = await response.json();
|
|
398
|
+
const stack = inferStack(repository);
|
|
399
|
+
fields.name.value = repository.name || parsed.repo;
|
|
400
|
+
fields.slug.value = slugify(repository.name || parsed.repo);
|
|
401
|
+
fields.description.value = repository.description || "";
|
|
402
|
+
fields.primaryStack.value = stack;
|
|
403
|
+
fields.tags.value = (repository.topics || []).slice(0, 8).join(", ");
|
|
404
|
+
setPlatforms(inferPlatforms(repository, stack));
|
|
405
|
+
status.textContent = "Draft generated.";
|
|
406
|
+
refreshYaml();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
status.textContent = error.message;
|
|
409
|
+
} finally {
|
|
410
|
+
fetchButton.disabled = false;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
document.querySelector("#submit-form").addEventListener("input", refreshYaml);
|
|
415
|
+
</script>
|
|
416
|
+
</BaseLayout>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@config "../../tailwind.config.mjs";
|
|
3
|
+
|
|
4
|
+
@layer base {
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
}
|
|
8
|
+
html {
|
|
9
|
+
-webkit-text-size-adjust: 100%;
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
@apply bg-white text-ink-900 dark:bg-ink-950 dark:text-ink-100 font-sans text-[15px] leading-relaxed;
|
|
15
|
+
}
|
|
16
|
+
a {
|
|
17
|
+
@apply text-ink-900 dark:text-ink-100;
|
|
18
|
+
}
|
|
19
|
+
/* Subtle selection */
|
|
20
|
+
::selection {
|
|
21
|
+
@apply bg-ink-200 text-ink-900 dark:bg-ink-800 dark:text-ink-100;
|
|
22
|
+
}
|
|
23
|
+
/* Slim, neutral scrollbar */
|
|
24
|
+
* {
|
|
25
|
+
scrollbar-width: thin;
|
|
26
|
+
scrollbar-color: theme('colors.ink.300') transparent;
|
|
27
|
+
}
|
|
28
|
+
*::-webkit-scrollbar {
|
|
29
|
+
width: 8px;
|
|
30
|
+
height: 8px;
|
|
31
|
+
}
|
|
32
|
+
*::-webkit-scrollbar-thumb {
|
|
33
|
+
@apply bg-ink-200 dark:bg-ink-800 rounded;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@layer components {
|
|
38
|
+
/* Section spacing — consistent vertical rhythm */
|
|
39
|
+
.section {
|
|
40
|
+
@apply py-12 sm:py-16;
|
|
41
|
+
}
|
|
42
|
+
.section-tight {
|
|
43
|
+
@apply py-10 sm:py-12;
|
|
44
|
+
}
|
|
45
|
+
/* Subtle card border + hover lift, used everywhere */
|
|
46
|
+
.card {
|
|
47
|
+
@apply rounded-lg border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-950 transition-[border-color,box-shadow,background-color] duration-150;
|
|
48
|
+
}
|
|
49
|
+
.card-hover {
|
|
50
|
+
@apply hover:border-ink-300 dark:hover:border-ink-700 hover:bg-ink-50/60 dark:hover:bg-ink-900/40 hover:shadow-[0_1px_2px_rgba(0,0,0,0.04),0_4px_12px_-2px_rgba(0,0,0,0.06)] dark:hover:shadow-[0_1px_2px_rgba(0,0,0,0.4),0_4px_12px_-2px_rgba(0,0,0,0.3)];
|
|
51
|
+
}
|
|
52
|
+
/* Pill badge — small, neutral */
|
|
53
|
+
.badge {
|
|
54
|
+
@apply inline-flex items-center gap-1 rounded border border-ink-200 dark:border-ink-800 bg-ink-50 dark:bg-ink-900/60 px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:text-ink-300;
|
|
55
|
+
}
|
|
56
|
+
.badge-accent {
|
|
57
|
+
@apply border-ink-900 dark:border-ink-100 bg-ink-900 dark:bg-ink-100 text-white dark:text-ink-900;
|
|
58
|
+
}
|
|
59
|
+
.badge-new {
|
|
60
|
+
@apply border-emerald-300/60 dark:border-emerald-800/60 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300;
|
|
61
|
+
}
|
|
62
|
+
.badge-hot {
|
|
63
|
+
@apply border-orange-300/60 dark:border-orange-800/60 bg-orange-50 dark:bg-orange-950/40 text-orange-800 dark:text-orange-300;
|
|
64
|
+
}
|
|
65
|
+
.badge-mature {
|
|
66
|
+
@apply border-blue-300/60 dark:border-blue-800/60 bg-blue-50 dark:bg-blue-950/40 text-blue-800 dark:text-blue-300;
|
|
67
|
+
}
|
|
68
|
+
.badge-stale {
|
|
69
|
+
@apply border-amber-300/60 dark:border-amber-800/60 bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300;
|
|
70
|
+
}
|
|
71
|
+
/* Outline button */
|
|
72
|
+
.btn {
|
|
73
|
+
@apply inline-flex items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors;
|
|
74
|
+
}
|
|
75
|
+
.btn-ghost {
|
|
76
|
+
@apply inline-flex items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors border-transparent text-ink-700 dark:text-ink-300 hover:bg-ink-100 dark:hover:bg-ink-900;
|
|
77
|
+
}
|
|
78
|
+
.btn-outline {
|
|
79
|
+
@apply inline-flex items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-950 text-ink-900 dark:text-ink-100 hover:border-ink-300 dark:hover:border-ink-700 hover:bg-ink-50 dark:hover:bg-ink-900/40;
|
|
80
|
+
}
|
|
81
|
+
.btn-primary {
|
|
82
|
+
@apply inline-flex items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors border-ink-900 dark:border-ink-100 bg-ink-900 dark:bg-ink-100 text-white dark:text-ink-900 hover:bg-ink-800 dark:hover:bg-white;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: ['./src/**/*.{astro,html,js,jsx,ts,tsx,md,mdx}'],
|
|
4
|
+
// Use a `.dark` class on <html> (toggled by the theme script in
|
|
5
|
+
// the document head) rather than the OS media query, so a manual
|
|
6
|
+
// theme switcher can override the user's system preference.
|
|
7
|
+
darkMode: 'class',
|
|
8
|
+
theme: {
|
|
9
|
+
// Disable default palette so we only ship our own neutral / accent
|
|
10
|
+
// tokens. Keeps the bundle small and forces a unified look.
|
|
11
|
+
colors: {
|
|
12
|
+
transparent: 'transparent',
|
|
13
|
+
current: 'currentColor',
|
|
14
|
+
white: '#ffffff',
|
|
15
|
+
black: '#000000',
|
|
16
|
+
ink: {
|
|
17
|
+
50: '#fafafa',
|
|
18
|
+
100: '#f4f4f5',
|
|
19
|
+
200: '#e4e4e7',
|
|
20
|
+
300: '#d4d4d8',
|
|
21
|
+
400: '#a1a1aa',
|
|
22
|
+
500: '#71717a',
|
|
23
|
+
600: '#52525b',
|
|
24
|
+
700: '#3f3f46',
|
|
25
|
+
800: '#27272a',
|
|
26
|
+
900: '#18181b',
|
|
27
|
+
950: '#09090b',
|
|
28
|
+
},
|
|
29
|
+
accent: {
|
|
30
|
+
DEFAULT: '#1f6feb',
|
|
31
|
+
muted: '#388bfd1a',
|
|
32
|
+
},
|
|
33
|
+
emerald: {
|
|
34
|
+
50: '#ecfdf5',
|
|
35
|
+
300: '#6ee7b7',
|
|
36
|
+
400: '#34d399',
|
|
37
|
+
700: '#047857',
|
|
38
|
+
800: '#065f46',
|
|
39
|
+
950: '#022c22',
|
|
40
|
+
},
|
|
41
|
+
orange: {
|
|
42
|
+
50: '#fff7ed',
|
|
43
|
+
300: '#fdba74',
|
|
44
|
+
400: '#fb923c',
|
|
45
|
+
700: '#c2410c',
|
|
46
|
+
800: '#9a3412',
|
|
47
|
+
950: '#431407',
|
|
48
|
+
},
|
|
49
|
+
blue: {
|
|
50
|
+
50: '#eff6ff',
|
|
51
|
+
300: '#93c5fd',
|
|
52
|
+
400: '#60a5fa',
|
|
53
|
+
700: '#1d4ed8',
|
|
54
|
+
800: '#1e40af',
|
|
55
|
+
950: '#172554',
|
|
56
|
+
},
|
|
57
|
+
amber: {
|
|
58
|
+
50: '#fffbeb',
|
|
59
|
+
300: '#fcd34d',
|
|
60
|
+
400: '#fbbf24',
|
|
61
|
+
700: '#b45309',
|
|
62
|
+
800: '#92400e',
|
|
63
|
+
950: '#451a03',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
extend: {
|
|
67
|
+
fontFamily: {
|
|
68
|
+
sans: [
|
|
69
|
+
'ui-sans-serif',
|
|
70
|
+
'system-ui',
|
|
71
|
+
'-apple-system',
|
|
72
|
+
'BlinkMacSystemFont',
|
|
73
|
+
'"Segoe UI"',
|
|
74
|
+
'Roboto',
|
|
75
|
+
'"Helvetica Neue"',
|
|
76
|
+
'Arial',
|
|
77
|
+
'sans-serif',
|
|
78
|
+
],
|
|
79
|
+
mono: [
|
|
80
|
+
'ui-monospace',
|
|
81
|
+
'SFMono-Regular',
|
|
82
|
+
'Menlo',
|
|
83
|
+
'Monaco',
|
|
84
|
+
'Consolas',
|
|
85
|
+
'"Liberation Mono"',
|
|
86
|
+
'"Courier New"',
|
|
87
|
+
'monospace',
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
fontSize: {
|
|
91
|
+
'2xs': ['0.6875rem', { lineHeight: '1rem' }],
|
|
92
|
+
},
|
|
93
|
+
maxWidth: {
|
|
94
|
+
container: '1140px',
|
|
95
|
+
wide: '1400px',
|
|
96
|
+
narrow: '720px',
|
|
97
|
+
},
|
|
98
|
+
borderRadius: {
|
|
99
|
+
DEFAULT: '0.375rem',
|
|
100
|
+
lg: '0.5rem',
|
|
101
|
+
xl: '0.75rem',
|
|
102
|
+
},
|
|
103
|
+
keyframes: {
|
|
104
|
+
'fade-in-up': {
|
|
105
|
+
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
|
106
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
107
|
+
},
|
|
108
|
+
'fade-in': {
|
|
109
|
+
'0%': { opacity: '0' },
|
|
110
|
+
'100%': { opacity: '1' },
|
|
111
|
+
},
|
|
112
|
+
'gradient-shift': {
|
|
113
|
+
'0%, 100%': { 'background-position': '0% 50%' },
|
|
114
|
+
'50%': { 'background-position': '100% 50%' },
|
|
115
|
+
},
|
|
116
|
+
'subtle-bob': {
|
|
117
|
+
'0%, 100%': { transform: 'translateY(0)' },
|
|
118
|
+
'50%': { transform: 'translateY(-2px)' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
animation: {
|
|
122
|
+
'fade-in-up': 'fade-in-up 0.6s ease-out both',
|
|
123
|
+
'fade-in': 'fade-in 0.8s ease-out both',
|
|
124
|
+
'gradient-shift': 'gradient-shift 8s ease-in-out infinite',
|
|
125
|
+
'subtle-bob': 'subtle-bob 3s ease-in-out infinite',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
plugins: [],
|
|
130
|
+
};
|