@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,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CuratedItem, HealthEntry, Decision } from "@grove-dev/core";
|
|
3
|
+
import HealthBadge from "./HealthBadge.astro";
|
|
4
|
+
import ScoreBars from "./ScoreBars.astro";
|
|
5
|
+
import { SCORE_LABELS, SCORE_REASONING } from "@grove-dev/ui";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
item: CuratedItem;
|
|
9
|
+
health?: HealthEntry;
|
|
10
|
+
decision?: Decision;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { item, health, decision } = Astro.props;
|
|
14
|
+
const links = Object.entries(item.links).filter(([, href]) => Boolean(href));
|
|
15
|
+
const channels = item.distribution.channels ?? [];
|
|
16
|
+
const languages = health?.github?.languages
|
|
17
|
+
? Object.entries(health.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
|
|
18
|
+
: [];
|
|
19
|
+
const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0));
|
|
20
|
+
const files = health?.github?.files ?? {};
|
|
21
|
+
const fileSignals = [
|
|
22
|
+
["README", files.readme],
|
|
23
|
+
["Contributing", files.contributing],
|
|
24
|
+
["Security", files.security],
|
|
25
|
+
["Issue templates", files.issueTemplates],
|
|
26
|
+
["PR template", files.pullRequestTemplate],
|
|
27
|
+
].filter(([, ok]) => ok !== undefined);
|
|
28
|
+
const monthly = health?.github?.monthlyCommits ?? [];
|
|
29
|
+
const maxCommits = Math.max(1, ...monthly.map((entry) => entry.commits));
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<article class="oc-detail">
|
|
33
|
+
<header class="oc-detail-header">
|
|
34
|
+
<div>
|
|
35
|
+
<p class="oc-eyebrow">{item.taxonomy.category}</p>
|
|
36
|
+
<h1>{item.name}</h1>
|
|
37
|
+
<p class="oc-lede">{item.description}</p>
|
|
38
|
+
</div>
|
|
39
|
+
<HealthBadge status={health?.health.status} />
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
<div class="oc-detail-grid">
|
|
43
|
+
<section class="oc-detail-wide">
|
|
44
|
+
<h2>Curation Scores</h2>
|
|
45
|
+
<ScoreBars scores={item.curation.scores} />
|
|
46
|
+
<dl class="oc-score-reasons">
|
|
47
|
+
{(Object.keys(SCORE_LABELS) as Array<keyof typeof SCORE_LABELS>).map((key) => (
|
|
48
|
+
<div>
|
|
49
|
+
<dt>{SCORE_LABELS[key]}</dt>
|
|
50
|
+
<dd>{SCORE_REASONING[key]}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</dl>
|
|
54
|
+
</section>
|
|
55
|
+
<section>
|
|
56
|
+
<h2>Links</h2>
|
|
57
|
+
<ul class="oc-link-list">
|
|
58
|
+
{links.map(([label, href]) => <li><a href={href}>{label}</a></li>)}
|
|
59
|
+
</ul>
|
|
60
|
+
{channels.length > 0 && (
|
|
61
|
+
<>
|
|
62
|
+
<h3>Distribution</h3>
|
|
63
|
+
<ul class="oc-link-list">
|
|
64
|
+
{channels.map((channel) => (
|
|
65
|
+
<li><a href={channel.url}>{channel.label ?? channel.type}</a>{channel.platform ? ` · ${channel.platform}` : ""}</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</section>
|
|
71
|
+
<section>
|
|
72
|
+
<h2>Metadata</h2>
|
|
73
|
+
<dl class="oc-fact-list">
|
|
74
|
+
<div><dt>Stars</dt><dd>{health?.github?.stars?.toLocaleString() ?? "Unknown"}</dd></div>
|
|
75
|
+
<div><dt>Forks</dt><dd>{health?.github?.forks?.toLocaleString() ?? "Unknown"}</dd></div>
|
|
76
|
+
<div><dt>License</dt><dd>{health?.github?.license ?? "Unknown"}</dd></div>
|
|
77
|
+
<div><dt>Language</dt><dd>{item.taxonomy.language ?? health?.github?.language ?? "Unknown"}</dd></div>
|
|
78
|
+
<div><dt>Last activity</dt><dd>{health?.github?.pushedAt?.slice(0, 10) ?? "Unknown"}</dd></div>
|
|
79
|
+
<div><dt>Latest release</dt><dd>{health?.github?.latestReleaseAt?.slice(0, 10) ?? "Unknown"}</dd></div>
|
|
80
|
+
<div><dt>Contributors</dt><dd>{health?.github?.contributorsKnown?.toLocaleString() ?? "Unknown"}</dd></div>
|
|
81
|
+
<div><dt>Open PRs</dt><dd>{health?.github?.openPullRequests?.toLocaleString() ?? "Unknown"}</dd></div>
|
|
82
|
+
</dl>
|
|
83
|
+
</section>
|
|
84
|
+
<section>
|
|
85
|
+
<h2>Health Signals</h2>
|
|
86
|
+
<ul class="oc-signal-list">
|
|
87
|
+
{(health?.health.reasons ?? ["No health metadata available"]).map((reason) => <li>{reason}</li>)}
|
|
88
|
+
</ul>
|
|
89
|
+
</section>
|
|
90
|
+
<section>
|
|
91
|
+
<h2>Curation</h2>
|
|
92
|
+
{decision ? (
|
|
93
|
+
<p><strong>{decision.decision.visibility.replace(/_/g, " ")}</strong>: {decision.decision.reason}</p>
|
|
94
|
+
) : (
|
|
95
|
+
<p>No manual curation decision has been recorded yet.</p>
|
|
96
|
+
)}
|
|
97
|
+
{item.curation.reviewed !== undefined && (
|
|
98
|
+
<p>{item.curation.reviewed ? "Curator reviewed" : "Generated curation pending review"}{item.curation.reviewedBy ? ` by ${item.curation.reviewedBy}` : ""}.</p>
|
|
99
|
+
)}
|
|
100
|
+
{item.source.file && <p class="oc-source">Source: {item.source.file}{item.source.line ? `:${item.source.line}` : ""}</p>}
|
|
101
|
+
</section>
|
|
102
|
+
{(item.curation.bestFor.length > 0 || item.curation.whyListed.length > 0 || item.curation.caveats.length > 0) && (
|
|
103
|
+
<section class="oc-detail-wide">
|
|
104
|
+
<h2>Why Included</h2>
|
|
105
|
+
<div class="oc-curation-columns">
|
|
106
|
+
{item.curation.bestFor.length > 0 && (
|
|
107
|
+
<div>
|
|
108
|
+
<h3>Best for</h3>
|
|
109
|
+
<ul class="oc-signal-list">{item.curation.bestFor.map((value) => <li>{value}</li>)}</ul>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
{item.curation.whyListed.length > 0 && (
|
|
113
|
+
<div>
|
|
114
|
+
<h3>Why listed</h3>
|
|
115
|
+
<ul class="oc-signal-list">{item.curation.whyListed.map((value) => <li>{value}</li>)}</ul>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
{item.curation.caveats.length > 0 && (
|
|
119
|
+
<div>
|
|
120
|
+
<h3>Caveats</h3>
|
|
121
|
+
<ul class="oc-signal-list">{item.curation.caveats.map((value) => <li>{value}</li>)}</ul>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</section>
|
|
126
|
+
)}
|
|
127
|
+
{(languages.length > 0 || fileSignals.length > 0 || monthly.length > 0) && (
|
|
128
|
+
<section class="oc-detail-wide">
|
|
129
|
+
<h2>Repository Signals</h2>
|
|
130
|
+
<div class="oc-repo-signals">
|
|
131
|
+
{languages.length > 0 && (
|
|
132
|
+
<div>
|
|
133
|
+
<h3>Languages</h3>
|
|
134
|
+
<dl class="oc-language-bars">
|
|
135
|
+
{languages.map(([language, bytes]) => (
|
|
136
|
+
<div>
|
|
137
|
+
<dt>{language}</dt>
|
|
138
|
+
<dd><span style={`width: ${(bytes / totalLanguageBytes) * 100}%`}></span></dd>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</dl>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
{fileSignals.length > 0 && (
|
|
145
|
+
<div>
|
|
146
|
+
<h3>Contribution files</h3>
|
|
147
|
+
<ul class="oc-signal-list">
|
|
148
|
+
{fileSignals.map(([label, ok]) => <li>{ok ? "Found" : "Missing"}: {label}</li>)}
|
|
149
|
+
</ul>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
{monthly.length > 0 && (
|
|
153
|
+
<div>
|
|
154
|
+
<h3>Monthly activity</h3>
|
|
155
|
+
<div class="oc-activity-bars">
|
|
156
|
+
{monthly.map((entry) => (
|
|
157
|
+
<span title={`${entry.month}: ${entry.commits} commits`} style={`height: ${Math.max(8, (entry.commits / maxCommits) * 48)}px`}></span>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</article>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Score } from "@grove-dev/core";
|
|
3
|
+
import { SCORE_LABELS, scoreTier } from "@grove-dev/ui";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
scores?: Score;
|
|
7
|
+
showOverall?: boolean;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { scores = {}, showOverall = true, compact = false } = Astro.props;
|
|
12
|
+
const dimensions = (["activity", "maturity", "learning", "contribution", "docs", "overall"] as const)
|
|
13
|
+
.filter((dimension) => showOverall || dimension !== "overall");
|
|
14
|
+
const hasAny = Object.values(scores).some((value) => typeof value === "number");
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<dl class="oc-scores" aria-label="Curation scores">
|
|
18
|
+
{!hasAny && <span class="oc-muted">Scores not yet curated</span>}
|
|
19
|
+
{hasAny && dimensions.map((dimension) => {
|
|
20
|
+
const value = scores[dimension];
|
|
21
|
+
const tier = scoreTier(value);
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<dt>{SCORE_LABELS[dimension]}</dt>
|
|
25
|
+
{!compact && <dd class="oc-score-value">{typeof value === "number" ? Math.round(value) : "-"}</dd>}
|
|
26
|
+
<dd class="oc-score-bar" aria-label={`${SCORE_LABELS[dimension]} ${value ?? 0} of 100`}>
|
|
27
|
+
{[0, 1, 2, 3].map((index) => <span class={index < tier ? "is-filled" : ""}></span>)}
|
|
28
|
+
</dd>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</dl>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
repoUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { repoUrl = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<section class="oc-submit-grid">
|
|
10
|
+
<form class="oc-submit-form" id="submit-form">
|
|
11
|
+
<label>
|
|
12
|
+
<span>GitHub URL</span>
|
|
13
|
+
<input id="repo-url" type="url" required placeholder="https://github.com/owner/repo" value={repoUrl} />
|
|
14
|
+
</label>
|
|
15
|
+
<div class="oc-two-col">
|
|
16
|
+
<label><span>Name</span><input id="project-name" type="text" /></label>
|
|
17
|
+
<label><span>Slug</span><input id="project-slug" type="text" /></label>
|
|
18
|
+
</div>
|
|
19
|
+
<label><span>Description</span><textarea id="project-description" rows="3"></textarea></label>
|
|
20
|
+
<div class="oc-two-col">
|
|
21
|
+
<label><span>Category</span><input id="project-category" type="text" placeholder="Developer Tools" /></label>
|
|
22
|
+
<label><span>Language</span><input id="project-language" type="text" placeholder="TypeScript" /></label>
|
|
23
|
+
</div>
|
|
24
|
+
<label><span>Tags</span><input id="project-tags" type="text" placeholder="cli, static-site, search" /></label>
|
|
25
|
+
<label><span>Best for</span><textarea id="project-best-for" rows="3" placeholder="One line per use case"></textarea></label>
|
|
26
|
+
</form>
|
|
27
|
+
<aside class="oc-submit-preview">
|
|
28
|
+
<div class="oc-section-header">
|
|
29
|
+
<h2>YAML draft</h2>
|
|
30
|
+
<button type="button" id="copy-yaml">Copy</button>
|
|
31
|
+
</div>
|
|
32
|
+
<pre><code id="yaml-preview">Paste a GitHub URL to generate a draft.</code></pre>
|
|
33
|
+
<p id="submit-status" aria-live="polite"></p>
|
|
34
|
+
</aside>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
type FormField = HTMLInputElement | HTMLTextAreaElement | HTMLElement | null;
|
|
39
|
+
|
|
40
|
+
const fields: Record<string, FormField> = {
|
|
41
|
+
repoUrl: document.querySelector("#repo-url"),
|
|
42
|
+
name: document.querySelector("#project-name"),
|
|
43
|
+
slug: document.querySelector("#project-slug"),
|
|
44
|
+
description: document.querySelector("#project-description"),
|
|
45
|
+
category: document.querySelector("#project-category"),
|
|
46
|
+
language: document.querySelector("#project-language"),
|
|
47
|
+
tags: document.querySelector("#project-tags"),
|
|
48
|
+
bestFor: document.querySelector("#project-best-for"),
|
|
49
|
+
preview: document.querySelector("#yaml-preview"),
|
|
50
|
+
status: document.querySelector("#submit-status"),
|
|
51
|
+
copy: document.querySelector("#copy-yaml"),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function valueOf(name: string): string {
|
|
55
|
+
const el = fields[name];
|
|
56
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return el.value;
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setText(name: string, text: string): void {
|
|
61
|
+
const el = fields[name];
|
|
62
|
+
if (el) el.textContent = text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function slugify(value: string): string {
|
|
66
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseRepo(value: string): { owner: string; repo: string } | null {
|
|
70
|
+
const match = String(value).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function list(values: string[], indent: number): string {
|
|
76
|
+
return values.length
|
|
77
|
+
? values.map((v) => `${" ".repeat(indent)}- ${v}`).join("\n")
|
|
78
|
+
: `${" ".repeat(indent)}[]`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function quote(value: string): string {
|
|
82
|
+
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generate(): void {
|
|
86
|
+
const repo = parseRepo(valueOf("repoUrl"));
|
|
87
|
+
if (!repo) {
|
|
88
|
+
setText("preview", "Paste a valid GitHub repository URL.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const name = valueOf("name") || repo.repo;
|
|
92
|
+
const slug = valueOf("slug") || slugify(name);
|
|
93
|
+
const tags = valueOf("tags")
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((t) => t.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
const bestFor = valueOf("bestFor")
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((l) => l.trim())
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
const yaml = [
|
|
102
|
+
`id: ${slug}`,
|
|
103
|
+
`name: ${quote(name)}`,
|
|
104
|
+
`description: ${quote(valueOf("description") || name)}`,
|
|
105
|
+
"links:",
|
|
106
|
+
` github: https://github.com/${repo.owner}/${repo.repo}`,
|
|
107
|
+
"source:",
|
|
108
|
+
" type: manual",
|
|
109
|
+
"taxonomy:",
|
|
110
|
+
` category: ${quote(valueOf("category") || "uncategorized")}`,
|
|
111
|
+
" tags:",
|
|
112
|
+
list(tags, 4),
|
|
113
|
+
...(valueOf("language") ? [` language: ${quote(valueOf("language"))}`] : []),
|
|
114
|
+
"curation:",
|
|
115
|
+
" bestFor:",
|
|
116
|
+
list(bestFor, 4),
|
|
117
|
+
" whyListed: []",
|
|
118
|
+
" caveats: []",
|
|
119
|
+
].join("\n");
|
|
120
|
+
setText("preview", yaml);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const input of Object.values(fields)) {
|
|
124
|
+
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
|
|
125
|
+
input.addEventListener("input", generate);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const copyBtn = fields.copy;
|
|
129
|
+
if (copyBtn) {
|
|
130
|
+
copyBtn.addEventListener("click", async () => {
|
|
131
|
+
const previewEl = fields.preview;
|
|
132
|
+
await navigator.clipboard.writeText(previewEl?.textContent ?? "");
|
|
133
|
+
setText("status", "Copied YAML draft.");
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
generate();
|
|
137
|
+
</script>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grove-dev/astro — Astro UI primitives for Grove.
|
|
3
|
+
*
|
|
4
|
+
* This package only owns Astro components, layouts, styles, and
|
|
5
|
+
* Astro-specific integrations. Generic filter/sort/stats/score
|
|
6
|
+
* helpers live in `@grove-dev/ui` and are re-exported from here for
|
|
7
|
+
* convenience.
|
|
8
|
+
*
|
|
9
|
+
* Astro components are not exported from this barrel — consumers
|
|
10
|
+
* import them by path, e.g.:
|
|
11
|
+
* import ItemCard from "@grove-dev/astro/components/ItemCard.astro";
|
|
12
|
+
* import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
13
|
+
*
|
|
14
|
+
* That keeps the barrel type-only and lets `astro check` validate
|
|
15
|
+
* the .astro files in their own context.
|
|
16
|
+
*/
|
|
17
|
+
export * from "@grove-dev/ui";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles.css";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { title, description = "A living, health-aware developer directory." } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<meta name="description" content={description} />
|
|
18
|
+
<title>{title}</title>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header class="oc-topbar">
|
|
22
|
+
<a href="/" class="oc-brand">Grove</a>
|
|
23
|
+
<nav aria-label="Primary">
|
|
24
|
+
<a href="/projects">Directory</a>
|
|
25
|
+
<a href="/methodology">Methodology</a>
|
|
26
|
+
</nav>
|
|
27
|
+
</header>
|
|
28
|
+
<main class="oc-shell">
|
|
29
|
+
<slot />
|
|
30
|
+
</main>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|