@grove-dev/astro 0.5.0-next.2 → 0.5.1
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/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +0 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/ui/button.d.ts +45 -0
- package/dist/ui/button.d.ts.map +1 -0
- package/dist/ui/button.js +82 -0
- package/dist/ui/button.js.map +1 -0
- package/package.json +9 -5
- package/src/components/CardGrid.astro +41 -0
- package/src/components/CardIcon.astro +67 -0
- package/src/components/CategoryGrid.astro +20 -5
- package/src/components/CollectionCard.astro +56 -0
- package/src/components/CollectionIndex.astro +21 -26
- package/src/components/CollectionPage.astro +36 -13
- package/src/components/CollectionRow.astro +62 -192
- package/src/components/CollectionTeaser.astro +4 -0
- package/src/components/ContributorsGrid.astro +4 -1
- package/src/components/DirectoryIndexClient.astro +127 -30
- package/src/components/EditorialSummary.astro +6 -5
- package/src/components/FilterGroupMenu.astro +24 -10
- package/src/components/FilterOptions.astro +6 -1
- package/src/components/FinalCta.astro +5 -3
- package/src/components/Hero.astro +85 -139
- package/src/components/IndexRow.astro +36 -69
- package/src/components/LanguageBreakdown.astro +5 -1
- package/src/components/OriginalCollection.astro +3 -2
- package/src/components/Pagination.astro +7 -20
- package/src/components/ProjectCard.astro +345 -0
- package/src/components/RecordHeader.astro +111 -82
- package/src/components/RecordSection.astro +14 -10
- package/src/components/RecordSidebar.astro +68 -20
- package/src/components/RefinePanel.astro +41 -62
- package/src/components/SmartLensTabs.astro +3 -7
- package/src/components/StackGrid.astro +26 -6
- package/src/components/SubmissionClient.astro +140 -63
- package/src/components/TableOfContents.astro +31 -5
- package/src/components/WhyThisExists.astro +1 -1
- package/src/index.ts +2 -2
- package/src/layouts/BaseLayout.astro +79 -9
- package/src/layouts/Header.astro +5 -4
- package/src/layouts/SectionHeader.astro +3 -2
- package/src/layouts/Seo.astro +28 -17
- package/src/layouts/ThemeToggle.astro +23 -6
- package/src/lib/index.ts +0 -2
- package/src/server/collections.ts +11 -0
- package/src/server/contrast.test.ts +82 -0
- package/src/server/contrast.ts +117 -0
- package/src/server/directory.test.ts +177 -0
- package/src/server/directory.ts +328 -206
- package/src/server/github-repo.test.ts +88 -0
- package/src/server/github-repo.ts +104 -0
- package/src/server/index.ts +4 -3
- package/src/server/models-home.test.ts +101 -0
- package/src/server/models.test.ts +135 -0
- package/src/server/models.ts +170 -42
- package/src/styles.css +128 -16
- package/src/ui/Badge.astro +38 -0
- package/src/ui/Button.astro +42 -0
- package/src/ui/EmptyState.astro +38 -0
- package/src/ui/FilterDrawer.astro +104 -0
- package/src/ui/PageHeader.astro +39 -0
- package/src/ui/SearchField.astro +50 -0
- package/src/ui/button.test.ts +54 -0
- package/src/ui/button.ts +98 -0
- package/dist/lib/scores.d.ts +0 -2
- package/dist/lib/scores.d.ts.map +0 -1
- package/dist/lib/scores.js +0 -2
- package/dist/lib/scores.js.map +0 -1
- package/src/components/CurationGrid.astro +0 -41
- package/src/components/DecisionRow.astro +0 -89
- package/src/components/ExploreByCategory.astro +0 -67
- package/src/components/ExploreByStack.astro +0 -78
- package/src/components/GroveDocumentHead.astro +0 -28
- package/src/components/ItemCard.astro +0 -324
- package/src/components/MinimalAbout.astro +0 -82
- package/src/components/ScoreBars.astro +0 -102
- package/src/lib/scores.ts +0 -1
|
@@ -83,25 +83,32 @@ const {
|
|
|
83
83
|
// items label so "apps" → "app" without an extra prop.
|
|
84
84
|
const singular = (itemLabel ?? itemsLabel.replace(/s$/, "")).toLowerCase();
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
...(stats.totalApps !== undefined
|
|
90
|
-
? [{
|
|
86
|
+
// One compact stats line: "6 projects indexed · 5 categories · 3 stacks".
|
|
87
|
+
// Every entry is guarded so a fresh directory never renders a zero-value.
|
|
88
|
+
const statParts: string[] = [
|
|
89
|
+
...(stats.totalApps !== undefined && stats.totalApps > 0
|
|
90
|
+
? [`${compact(stats.totalApps)} ${itemsLabel} indexed`]
|
|
91
91
|
: []),
|
|
92
|
-
...(stats.totalCategories !== undefined
|
|
93
|
-
? [{
|
|
92
|
+
...(stats.totalCategories !== undefined && stats.totalCategories > 0
|
|
93
|
+
? [`${stats.totalCategories} categories`]
|
|
94
94
|
: []),
|
|
95
|
-
...(stats.totalStacks !== undefined
|
|
96
|
-
? [{
|
|
95
|
+
...(stats.totalStacks !== undefined && stats.totalStacks > 0
|
|
96
|
+
? [`${stats.totalStacks} stacks`]
|
|
97
97
|
: []),
|
|
98
98
|
...(stats.totalOwners !== undefined && stats.totalOwners > 0
|
|
99
|
-
? [{
|
|
99
|
+
? [`${compact(stats.totalOwners)} owners`]
|
|
100
100
|
: []),
|
|
101
101
|
...(stats.totalStars !== undefined && stats.totalStars > 0
|
|
102
|
-
? [{
|
|
102
|
+
? [`${compact(stats.totalStars)} stars`]
|
|
103
103
|
: []),
|
|
104
104
|
];
|
|
105
|
+
|
|
106
|
+
// One primary CTA only — Submit already lives in the navbar
|
|
107
|
+
// (BaseLayout → Header submitHref), so the hero never duplicates it.
|
|
108
|
+
const primaryCta: CTA | undefined =
|
|
109
|
+
ctas.find((c) => c.variant === "primary") ??
|
|
110
|
+
ctas[0] ??
|
|
111
|
+
{ label: `Browse ${itemsLabel}`, href: searchHref, variant: "primary" };
|
|
105
112
|
---
|
|
106
113
|
|
|
107
114
|
<section
|
|
@@ -110,180 +117,119 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
110
117
|
className,
|
|
111
118
|
]}
|
|
112
119
|
>
|
|
120
|
+
{/* Dark mode only: a barely-there radial lift behind the headline. */}
|
|
113
121
|
<div
|
|
114
122
|
aria-hidden="true"
|
|
115
|
-
class="pointer-events-none absolute inset-0
|
|
116
|
-
style="
|
|
117
|
-
background-image:
|
|
118
|
-
radial-gradient(60% 50% at 15% 20%, color-mix(in oklab, var(--color-ink-300) 34%, transparent), transparent 60%),
|
|
119
|
-
radial-gradient(50% 40% at 85% 10%, color-mix(in oklab, var(--color-ink-200) 28%, transparent), transparent 60%),
|
|
120
|
-
radial-gradient(40% 40% at 50% 90%, color-mix(in oklab, var(--color-ink-400) 18%, transparent), transparent 60%);
|
|
121
|
-
"
|
|
123
|
+
class="pointer-events-none absolute inset-0 hidden dark:block"
|
|
124
|
+
style="background: radial-gradient(60% 55% at 50% 0%, color-mix(in oklab, var(--grove-foreground) 5%, transparent) 0%, transparent 70%);"
|
|
122
125
|
></div>
|
|
123
126
|
|
|
124
127
|
<Container>
|
|
125
|
-
<div class="relative py-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
aria-hidden="true"
|
|
132
|
-
class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"
|
|
133
|
-
></span>
|
|
134
|
-
{eyebrow}
|
|
135
|
-
{eyebrowBadge && (
|
|
136
|
-
<span
|
|
137
|
-
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-700 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
138
|
-
aria-label={eyebrowBadge}
|
|
139
|
-
>
|
|
140
|
-
{eyebrowBadge}
|
|
141
|
-
</span>
|
|
142
|
-
)}
|
|
143
|
-
</span>
|
|
144
|
-
|
|
145
|
-
<h1
|
|
146
|
-
class="mt-3 animate-fade-in-up text-[2.25rem] sm:text-[2.75rem] md:text-[3rem] font-semibold tracking-[-0.02em] leading-[1.05] text-ink-900 dark:text-ink-100 m-0"
|
|
147
|
-
style="animation-delay: 0.05s;"
|
|
148
|
-
>
|
|
149
|
-
{name}<br class="hidden sm:block" />
|
|
128
|
+
<div class="relative mx-auto flex max-w-[900px] flex-col items-center py-[88px] text-center lg:py-[104px]">
|
|
129
|
+
{/* Eyebrow */}
|
|
130
|
+
<span class="inline-flex animate-fade-in items-center gap-2 text-[13px] font-medium text-ink-500 dark:text-ink-400">
|
|
131
|
+
<span aria-hidden="true" class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"></span>
|
|
132
|
+
{eyebrow}
|
|
133
|
+
{eyebrowBadge && (
|
|
150
134
|
<span
|
|
151
|
-
class="
|
|
152
|
-
|
|
135
|
+
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
136
|
+
aria-label={eyebrowBadge}
|
|
153
137
|
>
|
|
154
|
-
{
|
|
138
|
+
{eyebrowBadge}
|
|
155
139
|
</span>
|
|
156
|
-
</h1>
|
|
157
|
-
|
|
158
|
-
{description && (
|
|
159
|
-
<p
|
|
160
|
-
class="mt-4 animate-fade-in-up text-[0.9375rem] sm:text-[1rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
|
|
161
|
-
style="animation-delay: 0.15s;"
|
|
162
|
-
>
|
|
163
|
-
{description}
|
|
164
|
-
</p>
|
|
165
140
|
)}
|
|
166
|
-
</
|
|
141
|
+
</span>
|
|
167
142
|
|
|
168
|
-
{/*
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
143
|
+
{/* Headline */}
|
|
144
|
+
<h1
|
|
145
|
+
class="m-0 mt-5 max-w-[820px] animate-fade-in-up text-balance text-[clamp(44px,5vw,64px)] font-[650] leading-[1.08] tracking-[-0.035em] text-ink-900 dark:text-ink-100"
|
|
146
|
+
style="animation-delay: 0.05s;"
|
|
147
|
+
>
|
|
148
|
+
<span class="block">{name}</span>
|
|
149
|
+
<span class="block">{tagline}</span>
|
|
150
|
+
</h1>
|
|
151
|
+
|
|
152
|
+
{description && (
|
|
153
|
+
<p
|
|
154
|
+
class="m-0 mt-5 max-w-[640px] animate-fade-in-up text-[16px] leading-[1.6] text-ink-500 dark:text-ink-400"
|
|
155
|
+
style="animation-delay: 0.1s;"
|
|
179
156
|
>
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
<dt class="sr-only">{s.label}</dt>
|
|
183
|
-
<dd class="m-0 font-mono text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
184
|
-
{s.value}
|
|
185
|
-
</dd>
|
|
186
|
-
<dd class="m-0 text-2xs uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
187
|
-
{s.label}
|
|
188
|
-
</dd>
|
|
189
|
-
</div>
|
|
190
|
-
))}
|
|
191
|
-
</dl>
|
|
157
|
+
{description}
|
|
158
|
+
</p>
|
|
192
159
|
)}
|
|
193
160
|
|
|
194
|
-
{/* Search
|
|
161
|
+
{/* Search — primary interaction. The FORM is the visual control:
|
|
162
|
+
it owns the border, radius, and the single focus treatment
|
|
163
|
+
(focus-within border + soft ring). The inner input opts out
|
|
164
|
+
of the global input styling (`grove-input-unstyled`) so it
|
|
165
|
+
stays a transparent, chrome-free text slot — no second box
|
|
166
|
+
or ring appearing inside the pill on focus. */}
|
|
195
167
|
<form
|
|
196
|
-
class="mt-
|
|
168
|
+
class="mt-8 flex h-[58px] w-full max-w-[840px] animate-fade-in-up items-center gap-3 rounded-xl border border-ink-200 bg-background p-1.5 pl-4 transition-[border-color,box-shadow] focus-within:border-ink-400 focus-within:ring-3 focus-within:ring-ring/15 dark:border-ink-800 dark:focus-within:border-ink-600"
|
|
169
|
+
style="animation-delay: 0.15s;"
|
|
197
170
|
role="search"
|
|
198
171
|
action={searchHref}
|
|
199
172
|
method="get"
|
|
200
173
|
>
|
|
201
|
-
<label class="
|
|
202
|
-
<span class="sr-only">Search {itemsLabel}, stacks, categories
|
|
203
|
-
<svg
|
|
204
|
-
|
|
205
|
-
width="16"
|
|
206
|
-
height="16"
|
|
207
|
-
aria-hidden="true"
|
|
208
|
-
class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
209
|
-
fill="currentColor"
|
|
210
|
-
>
|
|
211
|
-
<path
|
|
212
|
-
d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"
|
|
213
|
-
></path>
|
|
174
|
+
<label class="flex min-w-0 flex-1 items-center gap-3">
|
|
175
|
+
<span class="sr-only">Search {itemsLabel}, stacks, and categories</span>
|
|
176
|
+
<svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true" class="shrink-0 text-ink-400 dark:text-ink-500" fill="currentColor">
|
|
177
|
+
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
|
|
214
178
|
</svg>
|
|
215
179
|
<input
|
|
216
180
|
type="search"
|
|
217
181
|
name="q"
|
|
218
182
|
placeholder={searchPlaceholder}
|
|
219
|
-
class="h-
|
|
183
|
+
class="grove-input-unstyled h-full w-full min-w-0 rounded-none border-0 bg-transparent px-0 text-[16px] text-ink-900 outline-none placeholder:text-ink-400 dark:text-ink-100 dark:placeholder:text-ink-500"
|
|
220
184
|
/>
|
|
221
185
|
</label>
|
|
222
186
|
<button
|
|
223
187
|
type="submit"
|
|
224
|
-
class="inline-flex h-
|
|
188
|
+
class="inline-flex h-[46px] shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-primary px-5 text-[15px] font-medium leading-none text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"
|
|
225
189
|
>
|
|
226
190
|
Search
|
|
227
191
|
</button>
|
|
228
192
|
</form>
|
|
229
193
|
|
|
230
194
|
{quickFilters.length > 0 && (
|
|
231
|
-
<p
|
|
232
|
-
class="mt-2 text-2xs text-ink-500 dark:text-ink-400 m-0"
|
|
233
|
-
>
|
|
195
|
+
<p class="m-0 mt-4 text-[13px] text-ink-500 dark:text-ink-400">
|
|
234
196
|
Try{" "}
|
|
235
197
|
{quickFilters.map((f, i) => (
|
|
236
198
|
<>
|
|
237
|
-
<a
|
|
238
|
-
|
|
239
|
-
class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300"
|
|
240
|
-
>
|
|
241
|
-
{f.label}
|
|
242
|
-
</a>
|
|
243
|
-
{i < quickFilters.length - 1 && <span class="mx-1">·</span>}
|
|
199
|
+
<a href={f.href} class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">{f.label}</a>
|
|
200
|
+
{i < quickFilters.length - 1 && <span aria-hidden="true" class="mx-1">·</span>}
|
|
244
201
|
</>
|
|
245
202
|
))}
|
|
246
203
|
</p>
|
|
247
204
|
)}
|
|
248
205
|
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
258
|
-
: cta.variant === "ghost"
|
|
259
|
-
? "bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground"
|
|
260
|
-
: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
261
|
-
]}
|
|
262
|
-
{...(cta.external
|
|
263
|
-
? { target: "_blank", rel: "noopener noreferrer" }
|
|
264
|
-
: {})}
|
|
265
|
-
data-event="hero_cta_click"
|
|
266
|
-
data-event-source="hero"
|
|
267
|
-
>
|
|
268
|
-
{cta.label}
|
|
269
|
-
</a>
|
|
206
|
+
{/* Inline stats — single compact line, middle-dot separators */}
|
|
207
|
+
{statParts.length > 0 && (
|
|
208
|
+
<p class="m-0 mt-5 text-[13px] text-ink-500 dark:text-ink-400">
|
|
209
|
+
{statParts.map((part, i) => (
|
|
210
|
+
<>
|
|
211
|
+
{i > 0 && <span aria-hidden="true" class="mx-2 opacity-60">·</span>}
|
|
212
|
+
<span>{part}</span>
|
|
213
|
+
</>
|
|
270
214
|
))}
|
|
271
|
-
</
|
|
215
|
+
</p>
|
|
272
216
|
)}
|
|
273
217
|
|
|
274
|
-
{/*
|
|
275
|
-
{
|
|
276
|
-
<div class="mt-
|
|
277
|
-
<a href={searchHref} class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90">
|
|
278
|
-
Browse {itemsLabel}
|
|
279
|
-
</a>
|
|
218
|
+
{/* Primary CTA only */}
|
|
219
|
+
{primaryCta && (
|
|
220
|
+
<div class="mt-8">
|
|
280
221
|
<a
|
|
281
|
-
href={
|
|
282
|
-
|
|
283
|
-
rel
|
|
284
|
-
|
|
222
|
+
href={primaryCta.href}
|
|
223
|
+
class="inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-[var(--radius)] border border-transparent bg-primary px-5 text-[15px] font-medium leading-none text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"
|
|
224
|
+
{...(primaryCta.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
225
|
+
data-event="hero_cta_click"
|
|
226
|
+
data-event-source="hero"
|
|
285
227
|
>
|
|
286
|
-
|
|
228
|
+
{primaryCta.label}
|
|
229
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
230
|
+
<path d="M5 12h14"></path>
|
|
231
|
+
<path d="m12 5 7 7-7 7"></path>
|
|
232
|
+
</svg>
|
|
287
233
|
</a>
|
|
288
234
|
</div>
|
|
289
235
|
)}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
+
/**
|
|
3
|
+
* IndexRow — browse-page adapter around the canonical `ProjectCard`.
|
|
4
|
+
*
|
|
5
|
+
* Keeps the historical public props (`record`, `detailHref`,
|
|
6
|
+
* `showChips`, `eager`) so consumer pages copied from earlier versions
|
|
7
|
+
* keep working, while the visual language is now identical to every
|
|
8
|
+
* other card surface. The directory extras — curated badge, license,
|
|
9
|
+
* category · status line, labelled stack/platform chips, and the
|
|
10
|
+
* GitHub footer link — are enabled via `ProjectCard`'s opt-in props.
|
|
11
|
+
*/
|
|
2
12
|
import type { IndexRecord } from "@grove-dev/core";
|
|
3
|
-
import
|
|
4
|
-
import { formatRelative, formatStars, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds, statusDisplay, licenseDisplay } from "@grove-dev/astro";
|
|
13
|
+
import ProjectCard from "./ProjectCard.astro";
|
|
5
14
|
|
|
6
15
|
interface Props {
|
|
7
16
|
record: IndexRecord;
|
|
@@ -16,73 +25,31 @@ interface Props {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
const { record, detailHref, showChips = true, eager = false, class: className = "" } = Astro.props;
|
|
19
|
-
const item = record;
|
|
20
|
-
const project = item.kind === "project" ? item : null;
|
|
21
|
-
const name = item.kind === "resource" ? item.title : item.name;
|
|
22
|
-
const repoHref = project?.repoUrl ?? item.links.github;
|
|
23
|
-
const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
|
|
24
|
-
const avatar = project ? project.logoUrl ?? getOwnerAvatarUrl(owner, 80) : undefined;
|
|
25
|
-
const initials = name.split(/\s+/).map((word) => word[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();
|
|
26
|
-
const stacks = projectStackIds(project);
|
|
27
|
-
const platforms = project?.platforms ?? [];
|
|
28
|
-
const stars = project?.github?.stars;
|
|
29
|
-
const updatedAt = project?.github?.pushedAt ?? (item.kind === "resource" ? item.publishedAt : null);
|
|
30
|
-
const license = project?.github?.license;
|
|
31
|
-
const status = project?.health?.status;
|
|
32
|
-
const curated = Boolean(item.curation?.reviewed || (project && (project.bestFor.length || project.whyListed.length || project.caveats.length)));
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
<article class={`rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors hover:bg-secondary group relative flex flex-col gap-3 p-4 ${className}`}>
|
|
36
|
-
<header class="flex items-start gap-3">
|
|
37
|
-
<a href={detailHref} class="relative z-10 flex shrink-0" aria-label={`Open ${name} details`}>
|
|
38
|
-
{avatar ? (
|
|
39
|
-
<img
|
|
40
|
-
src={avatar}
|
|
41
|
-
alt={`${name} avatar`}
|
|
42
|
-
width="44"
|
|
43
|
-
height="44"
|
|
44
|
-
loading={eager ? "eager" : "lazy"}
|
|
45
|
-
fetchpriority={eager ? "high" : undefined}
|
|
46
|
-
decoding="async"
|
|
47
|
-
class="h-11 w-11 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
48
|
-
/>
|
|
49
|
-
) : (
|
|
50
|
-
<span class="flex h-11 w-11 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-sm font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300">{initials}</span>
|
|
51
|
-
)}
|
|
52
|
-
</a>
|
|
53
|
-
<div class="min-w-0 flex-1">
|
|
54
|
-
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
55
|
-
<h3 class="m-0 text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
56
|
-
<a href={detailHref} class="relative z-10 hover:underline">{name}</a>
|
|
57
|
-
</h3>
|
|
58
|
-
{curated && <span class="text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400">✓ curated</span>}
|
|
59
|
-
</div>
|
|
60
|
-
<div class="flex flex-wrap items-center gap-x-1.5 text-2xs text-ink-500 dark:text-ink-400">
|
|
61
|
-
{repoHref && owner && <a href={repoHref} target="_blank" rel="noopener noreferrer" class="relative z-10 font-mono hover:text-ink-900 dark:hover:text-ink-100">@{owner}{repo ? `/${repo}` : ""}</a>}
|
|
62
|
-
{owner && <span class="text-ink-300 dark:text-ink-700">·</span>}
|
|
63
|
-
<span>{item.category}</span>
|
|
64
|
-
{status && <><span class="text-ink-300 dark:text-ink-700">·</span><span>{statusDisplay(status)}</span></>}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</header>
|
|
68
28
|
|
|
69
|
-
|
|
29
|
+
const repoHref =
|
|
30
|
+
record.kind === "project" ? (record.repoUrl ?? record.links.github) : record.links.github;
|
|
70
31
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
32
|
+
const secondaryLinks = repoHref
|
|
33
|
+
? [
|
|
34
|
+
{
|
|
35
|
+
label: "GitHub",
|
|
36
|
+
href: repoHref,
|
|
37
|
+
external: true,
|
|
38
|
+
event: "external_repo_click",
|
|
39
|
+
eventSource: "index_row_github",
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
: [];
|
|
43
|
+
---
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
45
|
+
<ProjectCard
|
|
46
|
+
record={record}
|
|
47
|
+
href={detailHref}
|
|
48
|
+
showCurated
|
|
49
|
+
showLicense
|
|
50
|
+
showCategoryStatus
|
|
51
|
+
labelledChips={showChips}
|
|
52
|
+
secondaryLinks={secondaryLinks}
|
|
53
|
+
eager={eager}
|
|
54
|
+
class={className}
|
|
55
|
+
/>
|
|
@@ -38,7 +38,11 @@ const segments = languages.slice(0, maxSegments).map(([name, bytes]) => {
|
|
|
38
38
|
<h3 class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
39
39
|
Code composition
|
|
40
40
|
</h3>
|
|
41
|
-
<div
|
|
41
|
+
<div
|
|
42
|
+
class="grove-langbreak-bar mt-2 flex h-2 w-full overflow-hidden rounded-full bg-ink-100 dark:bg-ink-800"
|
|
43
|
+
role="img"
|
|
44
|
+
aria-label={`Code composition: ${segments.map((s) => `${s.name} ${s.pct}%`).join(', ')}`}
|
|
45
|
+
>
|
|
42
46
|
{segments.map((s) => (
|
|
43
47
|
<span
|
|
44
48
|
class="grove-langbreak-seg h-full"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Generic: takes plain `stats` and a `repoUrl`. No blueprint or
|
|
11
11
|
* site-specific copy.
|
|
12
12
|
*/
|
|
13
|
+
import { buttonClass } from "../ui/button.js";
|
|
13
14
|
|
|
14
15
|
interface OriginStats {
|
|
15
16
|
stars?: number;
|
|
@@ -54,7 +55,7 @@ const hasStats =
|
|
|
54
55
|
<section class:list={["py-12 sm:py-16", className]}>
|
|
55
56
|
<div class="mx-auto w-full max-w-container px-5 sm:px-7">
|
|
56
57
|
<div
|
|
57
|
-
class="rounded-[calc(var(--radius)+0.25rem)] border-
|
|
58
|
+
class="rounded-[calc(var(--radius)+0.25rem)] border border-border bg-card shadow-none transition-colors flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between"
|
|
58
59
|
>
|
|
59
60
|
<div class="flex flex-col gap-2">
|
|
60
61
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
@@ -104,7 +105,7 @@ const hasStats =
|
|
|
104
105
|
href={repoUrl}
|
|
105
106
|
target="_blank"
|
|
106
107
|
rel="noopener noreferrer"
|
|
107
|
-
class="
|
|
108
|
+
class={buttonClass("primary", "sm")}
|
|
108
109
|
data-event="original_collection_click"
|
|
109
110
|
data-event-source="original_collection"
|
|
110
111
|
>
|
|
@@ -6,24 +6,14 @@ interface Props {
|
|
|
6
6
|
class?: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { paginationPageList } from "@grove-dev/astro";
|
|
10
|
+
import { buttonClass, PAGINATION_EXTRA } from "../ui/button.js";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
if (count <= 7) return Array.from({ length: count }, (_, index) => index + 1);
|
|
13
|
-
const pages = new Set([1, count, active - 1, active, active + 1]);
|
|
14
|
-
const sorted = [...pages].filter((page) => page >= 1 && page <= count).sort((a, b) => a - b);
|
|
15
|
-
const result: Array<number | "ellipsis"> = [];
|
|
16
|
-
for (const page of sorted) {
|
|
17
|
-
const previous = result.at(-1);
|
|
18
|
-
if (typeof previous === "number" && page - previous > 1) result.push("ellipsis");
|
|
19
|
-
result.push(page);
|
|
20
|
-
}
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
12
|
+
const { current, total, hrefFor, class: className = "" } = Astro.props;
|
|
23
13
|
|
|
24
|
-
const pages =
|
|
25
|
-
const linkClass =
|
|
26
|
-
|
|
14
|
+
const pages = paginationPageList(current, total);
|
|
15
|
+
const linkClass = buttonClass("secondary", "sm", PAGINATION_EXTRA);
|
|
16
|
+
const currentClass = buttonClass("selected", "sm", PAGINATION_EXTRA);
|
|
27
17
|
---
|
|
28
18
|
|
|
29
19
|
{total > 1 && (
|
|
@@ -43,10 +33,7 @@ const linkClass =
|
|
|
43
33
|
) : (
|
|
44
34
|
<a
|
|
45
35
|
href={hrefFor(page)}
|
|
46
|
-
class
|
|
47
|
-
linkClass,
|
|
48
|
-
page === current && "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
49
|
-
]}
|
|
36
|
+
class={page === current ? currentClass : linkClass}
|
|
50
37
|
aria-current={page === current ? "page" : undefined}
|
|
51
38
|
>
|
|
52
39
|
{page}
|