@grove-dev/astro 0.1.5 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +306 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RecordSection.astro +88 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +193 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +263 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- package/templates/default/src/pages/contributors.astro +0 -142
|
@@ -1,416 +1,127 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
2
|
+
/**
|
|
3
|
+
* Submit page — text-based instructions for contributing a new
|
|
4
|
+
* record. No form: the openapps 19K form preview is overkill for
|
|
5
|
+
* the default template, and a static "open a PR / open an issue"
|
|
6
|
+
* workflow matches how the showcase actually accepts submissions.
|
|
7
|
+
*/
|
|
8
|
+
import siteConfig from "../../data/generated/site-config.json";
|
|
9
|
+
import { indexSlug, itemLabel } from "../data/records";
|
|
10
|
+
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
11
|
+
import Container from "@grove-dev/astro/layouts/Container.astro";
|
|
12
|
+
|
|
13
|
+
const title = `Submit a ${itemLabel()} — ${siteConfig.name}`;
|
|
14
|
+
const description = `Submit a new ${itemLabel()} to ${siteConfig.name}.`;
|
|
32
15
|
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>
|
|
16
|
+
const slug = indexSlug();
|
|
170
17
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
});
|
|
18
|
+
const repoPath = (siteConfig.repoUrl ?? "").replace(/^https?:\/\/github\.com\//, "");
|
|
19
|
+
const issueUrl = repoPath
|
|
20
|
+
? `https://github.com/${repoPath}/issues/new?template=record_submission.md`
|
|
21
|
+
: "https://github.com/tortuvshin/grove/issues/new";
|
|
22
|
+
---
|
|
413
23
|
|
|
414
|
-
|
|
415
|
-
|
|
24
|
+
<BaseLayout
|
|
25
|
+
title={title}
|
|
26
|
+
description={description}
|
|
27
|
+
site={siteConfig}
|
|
28
|
+
noindex={noindex}
|
|
29
|
+
>
|
|
30
|
+
<Container>
|
|
31
|
+
<article class="py-12 sm:py-16">
|
|
32
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
33
|
+
Submit
|
|
34
|
+
</span>
|
|
35
|
+
<h1 class="mt-1 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0">
|
|
36
|
+
Submit a {itemLabel()}
|
|
37
|
+
</h1>
|
|
38
|
+
<p class="mt-4 max-w-[62ch] text-sm text-ink-700 dark:text-ink-200 m-0">
|
|
39
|
+
{siteConfig.name} is a community knowledge site. Adding a new{" "}
|
|
40
|
+
{itemLabel()} is a two-step process — open a submission issue,
|
|
41
|
+
then open a pull request that adds a new YAML file under{" "}
|
|
42
|
+
<code class="font-mono text-ink-900 dark:text-ink-100">data/records/</code>.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<div class="mt-6 flex flex-wrap gap-2">
|
|
46
|
+
<a
|
|
47
|
+
href={issueUrl}
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
class="grove-btn grove-btn-primary"
|
|
51
|
+
>
|
|
52
|
+
Open a submission issue ↗
|
|
53
|
+
</a>
|
|
54
|
+
{repoPath && (
|
|
55
|
+
<a
|
|
56
|
+
href={`https://github.com/${repoPath}/blob/main/.github/ISSUE_TEMPLATE/record_submission.md`}
|
|
57
|
+
target="_blank"
|
|
58
|
+
rel="noopener noreferrer"
|
|
59
|
+
class="grove-btn grove-btn-outline"
|
|
60
|
+
>
|
|
61
|
+
See the issue template
|
|
62
|
+
</a>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<section class="mt-12 max-w-[68ch">
|
|
67
|
+
<h2 class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
68
|
+
Manual record (alternative to the issue form)
|
|
69
|
+
</h2>
|
|
70
|
+
<p class="mt-2 text-sm text-ink-700 dark:text-ink-200 m-0">
|
|
71
|
+
Prefer to write a record by hand? Here's the minimum shape for a{" "}
|
|
72
|
+
<code class="font-mono">{siteConfig.blueprint}</code> record.
|
|
73
|
+
The schema is validated automatically on every PR.
|
|
74
|
+
</p>
|
|
75
|
+
{siteConfig.blueprint === "project-directory" && (
|
|
76
|
+
<pre class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code>{`kind: project
|
|
77
|
+
slug: my-project
|
|
78
|
+
name: My Project
|
|
79
|
+
description: One-line description of what this project does
|
|
80
|
+
category: tools
|
|
81
|
+
tags:
|
|
82
|
+
- open-source
|
|
83
|
+
links:
|
|
84
|
+
github: https://github.com/owner/repo
|
|
85
|
+
website: https://example.com`}</code></pre>
|
|
86
|
+
)}
|
|
87
|
+
{siteConfig.blueprint === "resource-hub" && (
|
|
88
|
+
<pre class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code>{`kind: resource
|
|
89
|
+
slug: my-resource
|
|
90
|
+
title: My Resource
|
|
91
|
+
description: One-line description of what this resource is
|
|
92
|
+
category: guides
|
|
93
|
+
type: guide
|
|
94
|
+
topic: coding-agents
|
|
95
|
+
tags:
|
|
96
|
+
- ai
|
|
97
|
+
links:
|
|
98
|
+
source: https://example.com`}</code></pre>
|
|
99
|
+
)}
|
|
100
|
+
{siteConfig.blueprint === "ecosystem-map" && (
|
|
101
|
+
<pre class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code>{`kind: entity
|
|
102
|
+
slug: my-entity
|
|
103
|
+
name: My Organization
|
|
104
|
+
description: One-line description of what this organization does
|
|
105
|
+
category: education
|
|
106
|
+
type: company
|
|
107
|
+
tags:
|
|
108
|
+
- technology
|
|
109
|
+
links:
|
|
110
|
+
website: https://example.com`}</code></pre>
|
|
111
|
+
)}
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<section class="mt-12 max-w-[68ch">
|
|
115
|
+
<h2 class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
116
|
+
What happens after you submit
|
|
117
|
+
</h2>
|
|
118
|
+
<ol class="mt-3 m-0 flex list-decimal flex-col gap-2 pl-5 text-sm text-ink-700 dark:text-ink-200">
|
|
119
|
+
<li>A maintainer reviews the issue and tags it as <code class="font-mono">accepted</code> or <code class="font-mono">needs-info</code>.</li>
|
|
120
|
+
<li>You open a PR with the new <code class="font-mono">data/records/<slug>.yml</code> file.</li>
|
|
121
|
+
<li>CI runs <code class="font-mono">grove validate</code> and previews the site with your record.</li>
|
|
122
|
+
<li>A maintainer merges. The next <code class="font-mono">grove build</code> deploys it.</li>
|
|
123
|
+
</ol>
|
|
124
|
+
</section>
|
|
125
|
+
</article>
|
|
126
|
+
</Container>
|
|
416
127
|
</BaseLayout>
|
|
@@ -1,84 +1,58 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Grove V1 design tokens. Theme is overridable from `grove.config.ts`
|
|
5
|
+
* at scaffold time; we expose the same names as CSS variables so
|
|
6
|
+
* consumers can layer their own brand colors in without forking.
|
|
7
|
+
*
|
|
8
|
+
* The `ink-*` color scale is preserved from the V0 template because
|
|
9
|
+
* the page-level classes (`text-ink-900`, `bg-ink-50`, ...) still
|
|
10
|
+
* reference it. New pages should prefer the default Tailwind
|
|
11
|
+
* `gray-*` / `slate-*` palettes; the ink scale exists for
|
|
12
|
+
* back-compat with the V1 minimal template.
|
|
13
|
+
*/
|
|
14
|
+
@theme {
|
|
15
|
+
--color-ink-50: #fafafa;
|
|
16
|
+
--color-ink-100: #f4f4f5;
|
|
17
|
+
--color-ink-200: #e4e4e7;
|
|
18
|
+
--color-ink-300: #d4d4d8;
|
|
19
|
+
--color-ink-400: #a1a1aa;
|
|
20
|
+
--color-ink-500: #71717a;
|
|
21
|
+
--color-ink-600: #52525b;
|
|
22
|
+
--color-ink-700: #3f3f46;
|
|
23
|
+
--color-ink-800: #27272a;
|
|
24
|
+
--color-ink-900: #18181b;
|
|
25
|
+
--color-ink-950: #09090b;
|
|
26
|
+
|
|
27
|
+
--color-grove-primary: #16a34a;
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
@layer base {
|
|
5
31
|
:root {
|
|
32
|
+
--grove-color-primary: var(--color-grove-primary);
|
|
33
|
+
--grove-radius: 0.5rem;
|
|
34
|
+
--grove-container-width: 72rem;
|
|
35
|
+
|
|
6
36
|
color-scheme: light dark;
|
|
7
37
|
}
|
|
38
|
+
|
|
8
39
|
html {
|
|
9
40
|
-webkit-text-size-adjust: 100%;
|
|
10
41
|
-webkit-font-smoothing: antialiased;
|
|
11
42
|
-moz-osx-font-smoothing: grayscale;
|
|
12
43
|
}
|
|
44
|
+
|
|
13
45
|
body {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
background: #ffffff;
|
|
47
|
+
color: var(--color-ink-900);
|
|
48
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
49
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
50
|
+
font-size: 15px;
|
|
51
|
+
line-height: 1.55;
|
|
18
52
|
}
|
|
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
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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;
|
|
54
|
+
body.dark {
|
|
55
|
+
background: var(--color-ink-950);
|
|
56
|
+
color: var(--color-ink-100);
|
|
83
57
|
}
|
|
84
58
|
}
|