@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AppsIndexRow — compact table row for the list page.
|
|
4
|
+
*
|
|
5
|
+
* Renders: avatar + name + description + 3 metadata tiles
|
|
6
|
+
* (license, stars, updated) + chip row (stack/platform) + two
|
|
7
|
+
* CTAs (Details, GitHub). Designed for a dense table that the
|
|
8
|
+
* client-side filter script (and screen readers) can scan.
|
|
9
|
+
*
|
|
10
|
+
* `data-*` attributes are emitted so the consumer's filter script
|
|
11
|
+
* can read them and toggle visibility without re-rendering.
|
|
12
|
+
*
|
|
13
|
+
* Generic: takes a `Resource`. For non-project kinds the project
|
|
14
|
+
* fields are simply absent and the row degrades to a minimal
|
|
15
|
+
* name + description layout.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Resource, ProjectRecord } from "@grove-dev/core";
|
|
19
|
+
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../lib/repo";
|
|
20
|
+
import { formatRelative, formatStars } from "../lib/format";
|
|
21
|
+
import { statusDisplay, prettySlug } from "../lib/display";
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
record: Resource;
|
|
25
|
+
detailHref: string;
|
|
26
|
+
/** Whether to show stack/platform chips. Default true. */
|
|
27
|
+
showChips?: boolean;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
record,
|
|
33
|
+
detailHref,
|
|
34
|
+
showChips = true,
|
|
35
|
+
class: className = "",
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const isProject = record.kind === "project";
|
|
39
|
+
const project = isProject ? (record as ProjectRecord) : null;
|
|
40
|
+
|
|
41
|
+
const name =
|
|
42
|
+
record.kind === "resource"
|
|
43
|
+
? (record as { title: string }).title
|
|
44
|
+
: (record as { name: string }).name;
|
|
45
|
+
const description = record.description ?? "";
|
|
46
|
+
const repoUrl = project?.repoUrl;
|
|
47
|
+
const logoUrl = project?.logoUrl;
|
|
48
|
+
const stacks = project?.stacks ?? (project?.stack ? [project.stack] : []);
|
|
49
|
+
const platforms = project?.platforms ?? [];
|
|
50
|
+
const github = project?.github?.repository;
|
|
51
|
+
const stars = github?.stargazers_count;
|
|
52
|
+
const licenseSpdx = github?.license?.spdx_id;
|
|
53
|
+
const pushedAt = github?.pushed_at;
|
|
54
|
+
const healthStatus = project?.health?.status;
|
|
55
|
+
const githubUrl =
|
|
56
|
+
repoUrl ?? (record.links as { github?: string } | undefined)?.github;
|
|
57
|
+
|
|
58
|
+
const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
|
|
59
|
+
const avatarSrc =
|
|
60
|
+
logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
|
|
61
|
+
const initials = (name || "")
|
|
62
|
+
.replace(/[^a-z0-9]/gi, "")
|
|
63
|
+
.slice(0, 2)
|
|
64
|
+
.toUpperCase();
|
|
65
|
+
|
|
66
|
+
const allTags = [
|
|
67
|
+
...stacks.slice(0, 2).map((s) => `Stack: ${s}`),
|
|
68
|
+
...platforms.slice(0, 1).map((p) => `Platform: ${p}`),
|
|
69
|
+
];
|
|
70
|
+
const starsLabel = formatStars(stars);
|
|
71
|
+
const updatedLabel = formatRelative(pushedAt);
|
|
72
|
+
const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
|
|
73
|
+
|
|
74
|
+
const tags = record.tags ?? [];
|
|
75
|
+
const labels = record.curation?.labels ?? [];
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<article
|
|
79
|
+
class:list={[
|
|
80
|
+
"grove-row grove-card grid grid-cols-1 gap-3 p-4 sm:grid-cols-[auto_1fr_auto] sm:items-center",
|
|
81
|
+
className,
|
|
82
|
+
]}
|
|
83
|
+
data-record-slug={record.slug}
|
|
84
|
+
data-record-name={name}
|
|
85
|
+
data-record-kind={record.kind}
|
|
86
|
+
data-record-category={record.category}
|
|
87
|
+
data-record-stacks={stacks.join(",")}
|
|
88
|
+
data-record-platforms={platforms.join(",")}
|
|
89
|
+
data-record-tags={tags.join(",")}
|
|
90
|
+
data-record-labels={labels.join(",")}
|
|
91
|
+
data-record-search={[name, description, ownerRepo?.owner ?? "", ...stacks, ...platforms, ...tags, licenseSpdx ?? ""].join(" ").toLowerCase()}
|
|
92
|
+
>
|
|
93
|
+
<div class="flex items-center gap-3">
|
|
94
|
+
<div class="flex-shrink-0">
|
|
95
|
+
{avatarSrc ? (
|
|
96
|
+
<img
|
|
97
|
+
src={avatarSrc}
|
|
98
|
+
alt=""
|
|
99
|
+
width="32"
|
|
100
|
+
height="32"
|
|
101
|
+
loading="lazy"
|
|
102
|
+
decoding="async"
|
|
103
|
+
class="h-8 w-8 rounded-md border border-ink-200 object-contain bg-white dark:border-ink-800 dark:bg-ink-950"
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
<span
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-xs font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
|
|
109
|
+
>
|
|
110
|
+
{initials || "·"}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="min-w-0">
|
|
115
|
+
<h3 class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
116
|
+
<a href={detailHref} class="text-inherit no-underline hover:underline">
|
|
117
|
+
{name}
|
|
118
|
+
</a>
|
|
119
|
+
</h3>
|
|
120
|
+
{ownerRepo && (
|
|
121
|
+
<p class="m-0 mt-0.5 text-2xs text-ink-500 dark:text-ink-400">
|
|
122
|
+
@{ownerRepo.owner}/{ownerRepo.repo}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="min-w-0">
|
|
129
|
+
{description && (
|
|
130
|
+
<p class="m-0 line-clamp-2 text-[0.8125rem] leading-snug text-ink-500 dark:text-ink-400">
|
|
131
|
+
{description}
|
|
132
|
+
</p>
|
|
133
|
+
)}
|
|
134
|
+
{showChips && allTags.length > 0 && (
|
|
135
|
+
<ul class="mt-1.5 flex list-none flex-wrap gap-1.5 p-0">
|
|
136
|
+
{allTags.map((t) => <li class="grove-badge">{t}</li>)}
|
|
137
|
+
</ul>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<dl
|
|
142
|
+
class="grid grid-cols-3 gap-x-4 gap-y-1 text-2xs text-ink-500 dark:text-ink-400 sm:flex sm:flex-col sm:items-end sm:gap-y-0.5"
|
|
143
|
+
aria-label="Project signals"
|
|
144
|
+
>
|
|
145
|
+
{starsLabel && (
|
|
146
|
+
<div class="flex items-center gap-1">
|
|
147
|
+
<dt class="sr-only">Stars</dt>
|
|
148
|
+
<dd class="m-0 font-mono text-ink-700 dark:text-ink-200">{starsLabel} ★</dd>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{licenseSpdx && (
|
|
152
|
+
<div class="flex items-center gap-1">
|
|
153
|
+
<dt class="sr-only">License</dt>
|
|
154
|
+
<dd class="m-0">{licenseSpdx}</dd>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
{updatedLabel && updatedLabel !== "—" && (
|
|
158
|
+
<div class="flex items-center gap-1">
|
|
159
|
+
<dt class="sr-only">Last updated</dt>
|
|
160
|
+
<dd class="m-0">Updated {updatedLabel}</dd>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{healthLabel && (
|
|
164
|
+
<div class="flex items-center gap-1">
|
|
165
|
+
<dt class="sr-only">Health</dt>
|
|
166
|
+
<dd class="m-0">{healthLabel}</dd>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</dl>
|
|
170
|
+
|
|
171
|
+
<div class="col-span-full flex flex-wrap items-center gap-2 sm:col-span-1 sm:justify-end">
|
|
172
|
+
<a
|
|
173
|
+
href={detailHref}
|
|
174
|
+
class="grove-btn grove-btn-outline"
|
|
175
|
+
data-event="row_details_click"
|
|
176
|
+
data-event-source="apps_index_row"
|
|
177
|
+
data-event-app={record.slug}
|
|
178
|
+
>
|
|
179
|
+
View details
|
|
180
|
+
</a>
|
|
181
|
+
{githubUrl && (
|
|
182
|
+
<a
|
|
183
|
+
href={githubUrl}
|
|
184
|
+
target="_blank"
|
|
185
|
+
rel="noopener noreferrer"
|
|
186
|
+
class="grove-btn grove-btn-ghost"
|
|
187
|
+
data-event="row_repo_click"
|
|
188
|
+
data-event-source="apps_index_row"
|
|
189
|
+
data-event-app={record.slug}
|
|
190
|
+
>
|
|
191
|
+
GitHub
|
|
192
|
+
</a>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</article>
|
|
196
|
+
|
|
197
|
+
<style>
|
|
198
|
+
.line-clamp-2 {
|
|
199
|
+
display: -webkit-box;
|
|
200
|
+
-webkit-line-clamp: 2;
|
|
201
|
+
-webkit-box-orient: vertical;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AppsPagination — windowed page list.
|
|
4
|
+
*
|
|
5
|
+
* Renders a row of page links with first / prev / numeric / next /
|
|
6
|
+
* last. The numeric window is a 5-page sliding window centered on
|
|
7
|
+
* the current page (clamped at the ends). Pages with no links
|
|
8
|
+
* (gaps) are rendered as "…" to preserve visual rhythm.
|
|
9
|
+
*
|
|
10
|
+
* Generic: takes a `current` and `total` page count and an
|
|
11
|
+
* `hrefFor` builder. The href builder is the only consumer-side
|
|
12
|
+
* knowledge; no siteConfig import.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
current: number;
|
|
17
|
+
total: number;
|
|
18
|
+
hrefFor: (page: number) => string;
|
|
19
|
+
class?: string;
|
|
20
|
+
/** Window size for the numeric page list. Default 5. */
|
|
21
|
+
windowSize?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
current,
|
|
26
|
+
total,
|
|
27
|
+
hrefFor,
|
|
28
|
+
class: className = "",
|
|
29
|
+
windowSize = 5,
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
|
|
32
|
+
// Clamp current into [1, total].
|
|
33
|
+
const safeCurrent = Math.max(1, Math.min(total, current));
|
|
34
|
+
const half = Math.floor(windowSize / 2);
|
|
35
|
+
|
|
36
|
+
// Compute the numeric window. Clamp so it never extends past 1 or
|
|
37
|
+
// total, and never narrower than `windowSize` unless total is
|
|
38
|
+
// smaller.
|
|
39
|
+
function buildWindow(): (number | "…")[] {
|
|
40
|
+
if (total <= windowSize + 2) {
|
|
41
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
42
|
+
}
|
|
43
|
+
let start = Math.max(2, safeCurrent - half);
|
|
44
|
+
let end = Math.min(total - 1, safeCurrent + half);
|
|
45
|
+
// Expand the window toward the nearest edge if it shrinks.
|
|
46
|
+
if (end - start + 1 < windowSize) {
|
|
47
|
+
if (safeCurrent <= half + 1) {
|
|
48
|
+
end = Math.min(total - 1, end + (windowSize - (end - start + 1)));
|
|
49
|
+
} else {
|
|
50
|
+
start = Math.max(2, start - (windowSize - (end - start + 1)));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const items: (number | "…")[] = [1];
|
|
54
|
+
if (start > 2) items.push("…");
|
|
55
|
+
for (let i = start; i <= end; i++) items.push(i);
|
|
56
|
+
if (end < total - 1) items.push("…");
|
|
57
|
+
items.push(total);
|
|
58
|
+
return items;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pages = buildWindow();
|
|
62
|
+
const hasPrev = safeCurrent > 1;
|
|
63
|
+
const hasNext = safeCurrent < total;
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
<nav
|
|
67
|
+
class:list={[
|
|
68
|
+
"grove-pagination flex flex-wrap items-center gap-1",
|
|
69
|
+
className,
|
|
70
|
+
]}
|
|
71
|
+
aria-label="Pagination"
|
|
72
|
+
>
|
|
73
|
+
{hasPrev && (
|
|
74
|
+
<a
|
|
75
|
+
href={hrefFor(safeCurrent - 1)}
|
|
76
|
+
rel="prev"
|
|
77
|
+
class="grove-btn grove-btn-ghost"
|
|
78
|
+
data-event="pagination_click"
|
|
79
|
+
data-event-source="apps_pagination"
|
|
80
|
+
>
|
|
81
|
+
← Prev
|
|
82
|
+
</a>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<ol class="m-0 flex list-none items-center gap-1 p-0">
|
|
86
|
+
{pages.map((p) =>
|
|
87
|
+
p === "…" ? (
|
|
88
|
+
<li aria-hidden="true" class="px-2 text-2xs text-ink-400">
|
|
89
|
+
…
|
|
90
|
+
</li>
|
|
91
|
+
) : (
|
|
92
|
+
<li>
|
|
93
|
+
{p === safeCurrent ? (
|
|
94
|
+
<span
|
|
95
|
+
aria-current="page"
|
|
96
|
+
class="grove-btn grove-btn-primary pointer-events-none"
|
|
97
|
+
>
|
|
98
|
+
{p}
|
|
99
|
+
</span>
|
|
100
|
+
) : (
|
|
101
|
+
<a
|
|
102
|
+
href={hrefFor(p)}
|
|
103
|
+
class="grove-btn grove-btn-ghost"
|
|
104
|
+
data-event="pagination_click"
|
|
105
|
+
data-event-source="apps_pagination"
|
|
106
|
+
>
|
|
107
|
+
{p}
|
|
108
|
+
</a>
|
|
109
|
+
)}
|
|
110
|
+
</li>
|
|
111
|
+
),
|
|
112
|
+
)}
|
|
113
|
+
</ol>
|
|
114
|
+
|
|
115
|
+
{hasNext && (
|
|
116
|
+
<a
|
|
117
|
+
href={hrefFor(safeCurrent + 1)}
|
|
118
|
+
rel="next"
|
|
119
|
+
class="grove-btn grove-btn-ghost"
|
|
120
|
+
data-event="pagination_click"
|
|
121
|
+
data-event-source="apps_pagination"
|
|
122
|
+
>
|
|
123
|
+
Next →
|
|
124
|
+
</a>
|
|
125
|
+
)}
|
|
126
|
+
</nav>
|
|
@@ -1,27 +1,96 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* CategoryGrid — browse-by-category grid for the home page.
|
|
4
|
+
*
|
|
5
|
+
* The lighter sibling of StackGrid. Renders a card per category with
|
|
6
|
+
* name, blurb, and count. Generic: takes a list of plain
|
|
7
|
+
* `{ name, slug, blurb, count }` entries.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface CategoryEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
blurb?: string;
|
|
14
|
+
count?: number;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
interface Props {
|
|
5
|
-
categories:
|
|
18
|
+
categories: CategoryEntry[];
|
|
19
|
+
/** List page base. */
|
|
20
|
+
pathPrefix?: string;
|
|
21
|
+
/** Extra class. */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Hide the section header. */
|
|
24
|
+
bare?: boolean;
|
|
6
25
|
}
|
|
7
26
|
|
|
8
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
categories,
|
|
29
|
+
pathPrefix = "/",
|
|
30
|
+
class: className = "",
|
|
31
|
+
bare = false,
|
|
32
|
+
} = Astro.props;
|
|
33
|
+
|
|
34
|
+
function buildHref(slug: string): string {
|
|
35
|
+
const base = pathPrefix.replace(/\/$/, "");
|
|
36
|
+
return `${base}?category=${encodeURIComponent(slug)}`;
|
|
37
|
+
}
|
|
9
38
|
---
|
|
10
39
|
|
|
11
|
-
<section
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
<section
|
|
41
|
+
class:list={["grove-section", className]}
|
|
42
|
+
aria-labelledby={bare ? undefined : "grove-categorygrid-heading"}
|
|
43
|
+
>
|
|
44
|
+
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
45
|
+
{!bare && (
|
|
46
|
+
<header class="mb-6 flex flex-col gap-2">
|
|
47
|
+
<h2
|
|
48
|
+
id="grove-categorygrid-heading"
|
|
49
|
+
class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
50
|
+
>
|
|
51
|
+
Browse by category
|
|
52
|
+
</h2>
|
|
53
|
+
<p class="m-0 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
54
|
+
What kind of project are you looking for? Browse by category to find
|
|
55
|
+
reference apps, libraries, demos, and more.
|
|
56
|
+
</p>
|
|
57
|
+
</header>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
61
|
+
{categories.map((c) => (
|
|
62
|
+
<a
|
|
63
|
+
href={buildHref(c.slug)}
|
|
64
|
+
class="grove-card grove-card-hover group flex flex-col gap-1.5 p-4 no-underline"
|
|
65
|
+
data-event="category_grid_click"
|
|
66
|
+
data-event-source="category_grid"
|
|
67
|
+
>
|
|
68
|
+
<div class="flex items-baseline justify-between gap-2">
|
|
69
|
+
<h3 class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
70
|
+
{c.name}
|
|
71
|
+
</h3>
|
|
72
|
+
{typeof c.count === "number" && (
|
|
73
|
+
<span class="font-mono text-2xs text-ink-500 dark:text-ink-400">
|
|
74
|
+
{c.count}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{c.blurb && (
|
|
79
|
+
<p class="m-0 line-clamp-2 text-[0.8125rem] leading-snug text-ink-500 dark:text-ink-400">
|
|
80
|
+
{c.blurb}
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
</a>
|
|
84
|
+
))}
|
|
16
85
|
</div>
|
|
17
|
-
<a href="/projects">View all</a>
|
|
18
|
-
</div>
|
|
19
|
-
<div class="oc-category-grid">
|
|
20
|
-
{categories.map((category) => (
|
|
21
|
-
<a class="oc-category-card" href={`/categories/${slugForCategory(category.name)}`}>
|
|
22
|
-
<span>{category.name}</span>
|
|
23
|
-
<strong>{category.count}</strong>
|
|
24
|
-
</a>
|
|
25
|
-
))}
|
|
26
86
|
</div>
|
|
27
87
|
</section>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.line-clamp-2 {
|
|
91
|
+
display: -webkit-box;
|
|
92
|
+
-webkit-line-clamp: 2;
|
|
93
|
+
-webkit-box-orient: vertical;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ContributorsGrid — avatar grid for the home page.
|
|
4
|
+
*
|
|
5
|
+
* Renders a dense grid of contributor avatars with optional name and
|
|
6
|
+
* contribution count. Each tile links to the contributor's profile.
|
|
7
|
+
* No blueprint-specific terms — works for any "people" feed.
|
|
8
|
+
*
|
|
9
|
+
* Empty tiles are skipped. When the list is empty the entire section
|
|
10
|
+
* hides its body but keeps the header (so a "no contributors yet"
|
|
11
|
+
* state can be wired by a consumer via a slot).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface Contributor {
|
|
15
|
+
username: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
avatarUrl?: string;
|
|
18
|
+
profileUrl?: string;
|
|
19
|
+
contributions?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
contributors: Contributor[];
|
|
24
|
+
/** Total count to display in the header (e.g. "1,234 contributors"). */
|
|
25
|
+
total?: number;
|
|
26
|
+
/** Optional view-all href rendered top-right. */
|
|
27
|
+
viewAllHref?: string;
|
|
28
|
+
viewAllLabel?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
class?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
contributors,
|
|
36
|
+
total,
|
|
37
|
+
viewAllHref,
|
|
38
|
+
viewAllLabel = "View all",
|
|
39
|
+
title = "Top contributors",
|
|
40
|
+
description,
|
|
41
|
+
class: className = "",
|
|
42
|
+
} = Astro.props;
|
|
43
|
+
|
|
44
|
+
function fmt(n: number | undefined): string | null {
|
|
45
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
46
|
+
if (n >= 1000) {
|
|
47
|
+
const k = n / 1000;
|
|
48
|
+
return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
|
|
49
|
+
}
|
|
50
|
+
return n.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const visible = contributors
|
|
54
|
+
.filter((c) => c.username)
|
|
55
|
+
.slice(0, 24);
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<section
|
|
59
|
+
class:list={["grove-section", className]}
|
|
60
|
+
aria-labelledby="grove-contributors-heading"
|
|
61
|
+
>
|
|
62
|
+
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
63
|
+
<header class="mb-6 flex flex-wrap items-end justify-between gap-3">
|
|
64
|
+
<div>
|
|
65
|
+
<h2
|
|
66
|
+
id="grove-contributors-heading"
|
|
67
|
+
class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
68
|
+
>
|
|
69
|
+
{title}
|
|
70
|
+
{typeof total === "number" && (
|
|
71
|
+
<span class="ml-2 font-mono text-[0.9375rem] font-normal text-ink-500 dark:text-ink-400">
|
|
72
|
+
({fmt(total) ?? total})
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
</h2>
|
|
76
|
+
{description && (
|
|
77
|
+
<p class="m-0 mt-1 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
78
|
+
{description}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
{viewAllHref && (
|
|
83
|
+
<a
|
|
84
|
+
href={viewAllHref}
|
|
85
|
+
class="text-sm font-medium text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
86
|
+
>
|
|
87
|
+
{viewAllLabel} →
|
|
88
|
+
</a>
|
|
89
|
+
)}
|
|
90
|
+
</header>
|
|
91
|
+
|
|
92
|
+
{visible.length === 0 ? (
|
|
93
|
+
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
94
|
+
No contributors yet.
|
|
95
|
+
</p>
|
|
96
|
+
) : (
|
|
97
|
+
<ul
|
|
98
|
+
class="m-0 grid list-none grid-cols-3 gap-3 p-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8"
|
|
99
|
+
aria-label="Contributors"
|
|
100
|
+
>
|
|
101
|
+
{visible.map((c) => {
|
|
102
|
+
const href = c.profileUrl ?? `https://github.com/${c.username}`;
|
|
103
|
+
const avatar =
|
|
104
|
+
c.avatarUrl ??
|
|
105
|
+
`https://avatars.githubusercontent.com/${c.username}?v=4&s=80`;
|
|
106
|
+
return (
|
|
107
|
+
<li class="m-0 p-0">
|
|
108
|
+
<a
|
|
109
|
+
href={href}
|
|
110
|
+
target="_blank"
|
|
111
|
+
rel="noopener noreferrer"
|
|
112
|
+
class="group flex flex-col items-center gap-1.5 p-2 text-center no-underline"
|
|
113
|
+
data-event="contributor_click"
|
|
114
|
+
data-event-source="contributors_grid"
|
|
115
|
+
>
|
|
116
|
+
<img
|
|
117
|
+
src={avatar}
|
|
118
|
+
alt={c.name ?? c.username}
|
|
119
|
+
width="48"
|
|
120
|
+
height="48"
|
|
121
|
+
loading="lazy"
|
|
122
|
+
decoding="async"
|
|
123
|
+
class="h-12 w-12 rounded-full border border-ink-200 object-cover dark:border-ink-800"
|
|
124
|
+
/>
|
|
125
|
+
<span class="line-clamp-1 text-2xs font-medium text-ink-700 group-hover:text-ink-900 dark:text-ink-300 dark:group-hover:text-ink-100">
|
|
126
|
+
{c.name ?? c.username}
|
|
127
|
+
</span>
|
|
128
|
+
{typeof c.contributions === "number" && (
|
|
129
|
+
<span class="font-mono text-2xs text-ink-400">
|
|
130
|
+
{fmt(c.contributions) ?? c.contributions}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
</a>
|
|
134
|
+
</li>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</ul>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<style>
|
|
143
|
+
.line-clamp-1 {
|
|
144
|
+
display: -webkit-box;
|
|
145
|
+
-webkit-line-clamp: 1;
|
|
146
|
+
-webkit-box-orient: vertical;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* DecisionRow — a single row of the curation decision table.
|
|
4
|
+
*
|
|
5
|
+
* Used on the "Decisions" admin/curation page. Renders the record
|
|
6
|
+
* name, the current visibility/tier, the cleanup-candidate flag,
|
|
7
|
+
* and links to the source repo and detail page.
|
|
8
|
+
*
|
|
9
|
+
* Generic: takes a `Resource` and reads the standard fields. No
|
|
10
|
+
* blueprint-specific copy.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Resource, ProjectRecord } from "@grove-dev/core";
|
|
14
|
+
import { statusDisplay, prettySlug } from "../lib/display";
|
|
15
|
+
import { getOwnerAndRepoFromRepoUrl } from "../lib/repo";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
record: Resource;
|
|
19
|
+
detailHref?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { record, detailHref, class: className = "" } = Astro.props;
|
|
24
|
+
|
|
25
|
+
const isProject = record.kind === "project";
|
|
26
|
+
const project = isProject ? (record as ProjectRecord) : null;
|
|
27
|
+
|
|
28
|
+
const name =
|
|
29
|
+
record.kind === "resource"
|
|
30
|
+
? (record as { title: string }).title
|
|
31
|
+
: (record as { name: string }).name;
|
|
32
|
+
|
|
33
|
+
const repoUrl = project?.repoUrl;
|
|
34
|
+
const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
|
|
35
|
+
|
|
36
|
+
const visibility = project?.health?.visibility ?? "keep";
|
|
37
|
+
const tier = project?.health?.tier;
|
|
38
|
+
const status = project?.health?.status;
|
|
39
|
+
const cleanup = project?.health?.cleanupCandidate;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<tr
|
|
43
|
+
class:list={["grove-decision-row align-top text-sm", className]}
|
|
44
|
+
data-record-slug={record.slug}
|
|
45
|
+
>
|
|
46
|
+
<td class="px-3 py-2">
|
|
47
|
+
<div class="flex flex-col gap-0.5">
|
|
48
|
+
<a
|
|
49
|
+
href={detailHref ?? `/${record.slug}`}
|
|
50
|
+
class="font-medium text-ink-900 hover:underline dark:text-ink-100"
|
|
51
|
+
>
|
|
52
|
+
{name}
|
|
53
|
+
</a>
|
|
54
|
+
{ownerRepo && (
|
|
55
|
+
<span class="text-2xs text-ink-500 dark:text-ink-400">
|
|
56
|
+
@{ownerRepo.owner}/{ownerRepo.repo}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</td>
|
|
61
|
+
<td class="px-3 py-2 font-mono text-2xs text-ink-700 dark:text-ink-300">
|
|
62
|
+
{prettySlug(visibility)}
|
|
63
|
+
</td>
|
|
64
|
+
<td class="px-3 py-2 font-mono text-2xs text-ink-700 dark:text-ink-300">
|
|
65
|
+
{tier ? prettySlug(tier) : "—"}
|
|
66
|
+
</td>
|
|
67
|
+
<td class="px-3 py-2 font-mono text-2xs text-ink-700 dark:text-ink-300">
|
|
68
|
+
{status ? statusDisplay(status) : "—"}
|
|
69
|
+
</td>
|
|
70
|
+
<td class="px-3 py-2 text-2xs">
|
|
71
|
+
{cleanup ? (
|
|
72
|
+
<span class="grove-badge grove-badge-stale">cleanup</span>
|
|
73
|
+
) : (
|
|
74
|
+
<span class="text-ink-400">—</span>
|
|
75
|
+
)}
|
|
76
|
+
</td>
|
|
77
|
+
<td class="px-3 py-2 text-right">
|
|
78
|
+
{repoUrl && (
|
|
79
|
+
<a
|
|
80
|
+
href={repoUrl}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
class="text-2xs text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
84
|
+
>
|
|
85
|
+
repo →
|
|
86
|
+
</a>
|
|
87
|
+
)}
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|