@grove-dev/astro 0.5.0-next.2 → 0.5.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/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 +8 -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/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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProjectCard — the canonical card for one directory record.
|
|
4
|
+
*
|
|
5
|
+
* V1 (v0.5.0) refined design for the three-column grid, now shared by
|
|
6
|
+
* every listing surface (home sections, browse results, collection
|
|
7
|
+
* entries, taxonomy pages) through thin adapters (`IndexRow`,
|
|
8
|
+
* `CollectionRow`):
|
|
9
|
+
*
|
|
10
|
+
* - Default mode: the whole card is one clickable `<a>` (cursor:
|
|
11
|
+
* pointer, subtle border + surface lift on hover, no more than
|
|
12
|
+
* 1–2px vertical motion).
|
|
13
|
+
* - When `secondaryLinks` are passed the root becomes an `<article>`
|
|
14
|
+
* with a stretched title link, because nesting `<a>` inside `<a>`
|
|
15
|
+
* is invalid HTML. Secondary links sit above the stretched link
|
|
16
|
+
* (`relative z-10`).
|
|
17
|
+
* - App icon top-left (48px), app name + repository owner/repo,
|
|
18
|
+
* two-line description with reserved vertical space.
|
|
19
|
+
* - Footer row: platform logos (or labelled stack/platform chips via
|
|
20
|
+
* `labelledChips`), stars/updated/license on the right, and the
|
|
21
|
+
* navigation arrow anchored bottom-right.
|
|
22
|
+
* - Everything beyond the default home-page rendering is opt-in:
|
|
23
|
+
* `showCurated`, `showLicense`, `showCategoryStatus`,
|
|
24
|
+
* `labelledChips`, `secondaryLinks`, analytics `data-event` props.
|
|
25
|
+
*
|
|
26
|
+
* Data can come from a full `record` or from explicit props (adapters
|
|
27
|
+
* for shapes like `CollectionEntry` pass fields directly); explicit
|
|
28
|
+
* props win over record-derived values.
|
|
29
|
+
*
|
|
30
|
+
* Uses the package's design-system tokens plus the standard `dark:`
|
|
31
|
+
* variant. Equal-height cards in the row are achieved at the grid
|
|
32
|
+
* level (`CardGrid`: `grid` + `items-stretch`).
|
|
33
|
+
*/
|
|
34
|
+
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
35
|
+
import {
|
|
36
|
+
getOwnerAndRepoFromRepoUrl,
|
|
37
|
+
getOwnerAvatarUrl,
|
|
38
|
+
formatRelative,
|
|
39
|
+
formatStars,
|
|
40
|
+
licenseDisplay,
|
|
41
|
+
nameInitials,
|
|
42
|
+
projectStackIds,
|
|
43
|
+
statusDisplay,
|
|
44
|
+
} from "@grove-dev/astro";
|
|
45
|
+
import CardIcon from "./CardIcon.astro";
|
|
46
|
+
import Icon from "./Icon.astro";
|
|
47
|
+
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
48
|
+
|
|
49
|
+
type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
|
|
50
|
+
|
|
51
|
+
interface SecondaryLink {
|
|
52
|
+
label: string;
|
|
53
|
+
href: string;
|
|
54
|
+
/** Adds target="_blank" + rel and the external-link glyph. */
|
|
55
|
+
external?: boolean;
|
|
56
|
+
/** Analytics hooks forwarded as data-event attributes. */
|
|
57
|
+
event?: string;
|
|
58
|
+
eventSource?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
/** Record to render. Optional when explicit props are passed. */
|
|
63
|
+
record?: CardRecord;
|
|
64
|
+
/** Full detail-page URL. */
|
|
65
|
+
href: string;
|
|
66
|
+
/** Optional pre-resolved logo URL. Wins over the GitHub avatar. */
|
|
67
|
+
logoUrl?: string;
|
|
68
|
+
/** Override the description (defaults to `record.description`). */
|
|
69
|
+
description?: string;
|
|
70
|
+
/** Explicit overrides for record-derived values (adapter use). */
|
|
71
|
+
name?: string;
|
|
72
|
+
slug?: string;
|
|
73
|
+
repoUrl?: string;
|
|
74
|
+
stars?: number;
|
|
75
|
+
pushedAt?: string | null;
|
|
76
|
+
license?: string | null;
|
|
77
|
+
category?: string;
|
|
78
|
+
status?: string;
|
|
79
|
+
curated?: boolean;
|
|
80
|
+
stack?: string;
|
|
81
|
+
platforms?: string[];
|
|
82
|
+
/** Opt-in extras (all default off to keep the home grid unchanged). */
|
|
83
|
+
showCurated?: boolean;
|
|
84
|
+
showLicense?: boolean;
|
|
85
|
+
showCategoryStatus?: boolean;
|
|
86
|
+
/** Labelled stack/platform chip rows instead of the logo strip. */
|
|
87
|
+
labelledChips?: boolean;
|
|
88
|
+
/** Footer links; presence switches the root to `<article>` mode. */
|
|
89
|
+
secondaryLinks?: SecondaryLink[];
|
|
90
|
+
/** Eager-load the avatar (above-the-fold rows). */
|
|
91
|
+
eager?: boolean;
|
|
92
|
+
/** Analytics data-event attributes on the card root. */
|
|
93
|
+
event?: string;
|
|
94
|
+
eventSource?: string;
|
|
95
|
+
class?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
record,
|
|
100
|
+
href,
|
|
101
|
+
logoUrl: logoUrlProp,
|
|
102
|
+
description: descriptionProp,
|
|
103
|
+
name: nameProp,
|
|
104
|
+
slug: slugProp,
|
|
105
|
+
repoUrl: repoUrlProp,
|
|
106
|
+
stars: starsProp,
|
|
107
|
+
pushedAt: pushedAtProp,
|
|
108
|
+
license: licenseProp,
|
|
109
|
+
category: categoryProp,
|
|
110
|
+
status: statusProp,
|
|
111
|
+
curated: curatedProp,
|
|
112
|
+
stack: stackProp,
|
|
113
|
+
platforms: platformsProp,
|
|
114
|
+
showCurated = false,
|
|
115
|
+
showLicense = false,
|
|
116
|
+
showCategoryStatus = false,
|
|
117
|
+
labelledChips = false,
|
|
118
|
+
secondaryLinks = [],
|
|
119
|
+
eager = false,
|
|
120
|
+
event,
|
|
121
|
+
eventSource,
|
|
122
|
+
class: className = "",
|
|
123
|
+
} = Astro.props;
|
|
124
|
+
|
|
125
|
+
const isProject = record?.kind === "project";
|
|
126
|
+
const name =
|
|
127
|
+
nameProp ??
|
|
128
|
+
(record
|
|
129
|
+
? record.kind === "resource"
|
|
130
|
+
? (record as { title: string }).title
|
|
131
|
+
: (record as { name: string }).name
|
|
132
|
+
: "");
|
|
133
|
+
const slug = slugProp ?? (record as { slug?: string } | undefined)?.slug;
|
|
134
|
+
const repoHref =
|
|
135
|
+
repoUrlProp ??
|
|
136
|
+
(isProject
|
|
137
|
+
? (record as { repoUrl?: string }).repoUrl
|
|
138
|
+
: (record as { links?: { github?: string } } | undefined)?.links?.github);
|
|
139
|
+
|
|
140
|
+
const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
|
|
141
|
+
const avatarUrl =
|
|
142
|
+
logoUrlProp ??
|
|
143
|
+
(isProject ? (record as { logoUrl?: string }).logoUrl : undefined) ??
|
|
144
|
+
getOwnerAvatarUrl(owner, 80);
|
|
145
|
+
|
|
146
|
+
const initials = nameInitials(name);
|
|
147
|
+
|
|
148
|
+
const description =
|
|
149
|
+
descriptionProp ?? (record as { description?: string } | undefined)?.description ?? "";
|
|
150
|
+
|
|
151
|
+
const platforms =
|
|
152
|
+
platformsProp ?? (isProject ? ((record as { platforms?: string[] }).platforms ?? []) : []);
|
|
153
|
+
const stars =
|
|
154
|
+
starsProp ?? (isProject ? (record as { github?: { stars?: number } }).github?.stars : undefined);
|
|
155
|
+
const pushedAt =
|
|
156
|
+
pushedAtProp ??
|
|
157
|
+
(isProject
|
|
158
|
+
? (record as { github?: { pushedAt?: string | null } }).github?.pushedAt
|
|
159
|
+
: (record as { publishedAt?: string | null } | undefined)?.publishedAt);
|
|
160
|
+
const license =
|
|
161
|
+
licenseProp ??
|
|
162
|
+
(isProject ? (record as { github?: { license?: string | null } }).github?.license : undefined);
|
|
163
|
+
const category = categoryProp ?? (record as { category?: string } | undefined)?.category;
|
|
164
|
+
const status =
|
|
165
|
+
statusProp ?? (isProject ? (record as { health?: { status?: string } }).health?.status : undefined);
|
|
166
|
+
const curated =
|
|
167
|
+
curatedProp ??
|
|
168
|
+
Boolean(
|
|
169
|
+
(record as { curation?: { reviewed?: boolean } } | undefined)?.curation?.reviewed ||
|
|
170
|
+
(isProject &&
|
|
171
|
+
((record as { bestFor?: string[] }).bestFor?.length ||
|
|
172
|
+
(record as { whyListed?: string[] }).whyListed?.length ||
|
|
173
|
+
(record as { caveats?: string[] }).caveats?.length)),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const stackId =
|
|
177
|
+
stackProp ??
|
|
178
|
+
(isProject
|
|
179
|
+
? projectStackIds(record as { stack?: string; stacks?: string[] })[0]
|
|
180
|
+
: undefined);
|
|
181
|
+
|
|
182
|
+
const starsLabel = formatStars(stars);
|
|
183
|
+
const updatedLabel = formatRelative(pushedAt);
|
|
184
|
+
// "Unknown" is an internal placeholder, not information — suppress it
|
|
185
|
+
// so card metadata never reads "Category · Unknown" to visitors.
|
|
186
|
+
const rawStatusText = status ? statusDisplay(status as Parameters<typeof statusDisplay>[0]) : "";
|
|
187
|
+
const statusText = rawStatusText === "Unknown" ? "" : rawStatusText;
|
|
188
|
+
const licenseLabel = license ? licenseDisplay(license) : null;
|
|
189
|
+
|
|
190
|
+
const hasStars = starsLabel !== null && typeof stars === "number" && stars > 0;
|
|
191
|
+
const hasUpdated = Boolean(pushedAt);
|
|
192
|
+
|
|
193
|
+
// `<a>`-in-`<a>` is invalid HTML, so cards with secondary links render
|
|
194
|
+
// as an `<article>` whose title link is stretched over the card.
|
|
195
|
+
const isArticle = secondaryLinks.length > 0;
|
|
196
|
+
const Root = isArticle ? "article" : "a";
|
|
197
|
+
const rootAttrs = isArticle ? {} : { href, "aria-label": `Open ${name}` };
|
|
198
|
+
|
|
199
|
+
// Render the homepage brand strip through the shared `Icon` component
|
|
200
|
+
// so brand marks come from the same `/icons/platforms/` files the
|
|
201
|
+
// chip rows use. No more inline SVG strings — one source of truth.
|
|
202
|
+
// `Icon` falls back to initials when an asset is missing, so the
|
|
203
|
+
// strip simply renders whatever platforms the record declares.
|
|
204
|
+
const visiblePlatforms = platforms.slice(0, 4);
|
|
205
|
+
const platformOverflow = platforms.length - visiblePlatforms.length;
|
|
206
|
+
const hasPlatforms = visiblePlatforms.length > 0;
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
<Root
|
|
210
|
+
{...rootAttrs}
|
|
211
|
+
class={`groove-project-card group relative flex h-full min-w-0 flex-col gap-3 rounded-xl border border-ink-200 bg-card p-5 outline-none transition-[border-color,background-color,transform] duration-150 hover:-translate-y-0.5 hover:border-ink-300 hover:bg-ink-50 focus-visible:border-ink-500 focus-visible:ring-2 focus-visible:ring-ink-900/15 dark:border-ink-800 dark:hover:border-ink-700 dark:hover:bg-ink-900 dark:focus-visible:border-ink-500 dark:focus-visible:ring-ink-100/15 ${className}`}
|
|
212
|
+
data-record-slug={slug}
|
|
213
|
+
data-event={event}
|
|
214
|
+
data-event-source={eventSource}
|
|
215
|
+
data-event-item={event ? slug : undefined}
|
|
216
|
+
>
|
|
217
|
+
{/* Header: icon + name + owner */}
|
|
218
|
+
<header class="flex items-start gap-3">
|
|
219
|
+
{avatarUrl ? (
|
|
220
|
+
<img
|
|
221
|
+
src={avatarUrl}
|
|
222
|
+
alt=""
|
|
223
|
+
width="48"
|
|
224
|
+
height="48"
|
|
225
|
+
loading={eager ? "eager" : "lazy"}
|
|
226
|
+
fetchpriority={eager ? "high" : undefined}
|
|
227
|
+
decoding="async"
|
|
228
|
+
class="h-12 w-12 shrink-0 rounded-lg bg-white object-contain p-1 dark:bg-white"
|
|
229
|
+
/>
|
|
230
|
+
) : (
|
|
231
|
+
<span
|
|
232
|
+
aria-hidden="true"
|
|
233
|
+
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-ink-100 text-base font-semibold uppercase text-ink-700 dark:bg-ink-800 dark:text-ink-200"
|
|
234
|
+
>
|
|
235
|
+
{initials}
|
|
236
|
+
</span>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
240
|
+
<h3 class="m-0 truncate text-[17px] font-semibold leading-snug tracking-tight text-ink-900 sm:text-[18px] dark:text-ink-100">
|
|
241
|
+
{isArticle ? (
|
|
242
|
+
<a
|
|
243
|
+
href={href}
|
|
244
|
+
class="text-inherit no-underline before:absolute before:inset-0 before:z-0 before:content-['']"
|
|
245
|
+
>
|
|
246
|
+
{name}
|
|
247
|
+
</a>
|
|
248
|
+
) : (
|
|
249
|
+
name
|
|
250
|
+
)}
|
|
251
|
+
</h3>
|
|
252
|
+
{owner && repo && (
|
|
253
|
+
<p class="m-0 truncate font-mono text-[13px] text-ink-500 dark:text-ink-400">
|
|
254
|
+
{owner}<span class="opacity-60">/</span>{repo}
|
|
255
|
+
</p>
|
|
256
|
+
)}
|
|
257
|
+
{showCategoryStatus && (category || statusText) && (
|
|
258
|
+
<p class="m-0 truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
259
|
+
{category}
|
|
260
|
+
{category && statusText && <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>}
|
|
261
|
+
{statusText}
|
|
262
|
+
</p>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{showCurated && curated && (
|
|
267
|
+
<span
|
|
268
|
+
class="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
269
|
+
title="Reviewed by a curator"
|
|
270
|
+
>
|
|
271
|
+
<CardIcon name="curated" />
|
|
272
|
+
curated
|
|
273
|
+
</span>
|
|
274
|
+
)}
|
|
275
|
+
</header>
|
|
276
|
+
|
|
277
|
+
{/* Description — two-line clamp with reserved vertical space */}
|
|
278
|
+
<p
|
|
279
|
+
class="m-0 line-clamp-2 min-h-[42px] text-[14px] leading-[1.5] text-ink-500 dark:text-ink-400"
|
|
280
|
+
>
|
|
281
|
+
{description}
|
|
282
|
+
</p>
|
|
283
|
+
|
|
284
|
+
{/* Labelled stack/platform chip rows (opt-in; directory surfaces) */}
|
|
285
|
+
{labelledChips && (
|
|
286
|
+
<StackPlatformChips stack={stackId} platform={platforms} />
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Footer: links / platform logos · meta · arrow */}
|
|
290
|
+
<footer class="mt-auto flex items-center justify-between gap-3 text-[12px] sm:text-[13px]">
|
|
291
|
+
<div class="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-ink-500 dark:text-ink-400">
|
|
292
|
+
{secondaryLinks.map((link) => (
|
|
293
|
+
<a
|
|
294
|
+
href={link.href}
|
|
295
|
+
{...(link.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
296
|
+
data-event={link.event}
|
|
297
|
+
data-event-source={link.eventSource}
|
|
298
|
+
data-event-item={link.event ? slug : undefined}
|
|
299
|
+
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-700 no-underline transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
300
|
+
>
|
|
301
|
+
{link.label}
|
|
302
|
+
{link.external && <CardIcon name="external" />}
|
|
303
|
+
</a>
|
|
304
|
+
))}
|
|
305
|
+
{!labelledChips && hasPlatforms && (
|
|
306
|
+
<div class="flex items-center gap-2.5">
|
|
307
|
+
{visiblePlatforms.map((platform) => (
|
|
308
|
+
<Icon
|
|
309
|
+
name={platform}
|
|
310
|
+
category="platform"
|
|
311
|
+
size={22}
|
|
312
|
+
title={platform}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
{platformOverflow > 0 && (
|
|
316
|
+
<span class="text-[12px] text-ink-500 dark:text-ink-400">+{platformOverflow}</span>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="flex min-w-0 shrink-0 items-center gap-3 text-ink-500 dark:text-ink-400">
|
|
323
|
+
{hasStars && (
|
|
324
|
+
<span class="inline-flex items-center gap-1" title={`${stars ?? 0} stars on GitHub`}>
|
|
325
|
+
<span class="inline-flex text-amber-500"><CardIcon name="star" /></span>
|
|
326
|
+
<span class="font-mono">{starsLabel}</span>
|
|
327
|
+
</span>
|
|
328
|
+
)}
|
|
329
|
+
{hasUpdated && (
|
|
330
|
+
<span class="whitespace-nowrap" title={`Last updated ${pushedAt}`}>
|
|
331
|
+
Updated {updatedLabel}
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
{showLicense && licenseLabel && (
|
|
335
|
+
<span class="font-mono text-[10px] uppercase tracking-wide" title={`License: ${licenseLabel}`}>
|
|
336
|
+
{licenseLabel}
|
|
337
|
+
</span>
|
|
338
|
+
)}
|
|
339
|
+
<CardIcon
|
|
340
|
+
name="arrow"
|
|
341
|
+
class="shrink-0 text-ink-400 transition-colors group-hover:text-ink-700 dark:text-ink-500 dark:group-hover:text-ink-100"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
</footer>
|
|
345
|
+
</Root>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* the badge's `data-activity-tone` attribute picks one at runtime.
|
|
22
22
|
*/
|
|
23
23
|
import type { RecordDetailModel } from "../server/models.js";
|
|
24
|
+
import { buttonClass } from "../ui/button.js";
|
|
24
25
|
|
|
25
26
|
interface Props {
|
|
26
27
|
detail: RecordDetailModel;
|
|
@@ -40,34 +41,64 @@ const {
|
|
|
40
41
|
sourceDescription,
|
|
41
42
|
screenshots,
|
|
42
43
|
category,
|
|
43
|
-
description,
|
|
44
44
|
name,
|
|
45
45
|
} = detail;
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
<header class="mb-
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
<header class="mb-8">
|
|
49
|
+
{/* Identity row: logo sized to the name+repo block beside it — no
|
|
50
|
+
tall avatar column leaving dead space under the logo. Pills sit
|
|
51
|
+
at the row's right edge on wide viewports and wrap below on
|
|
52
|
+
mobile. */}
|
|
53
|
+
<div class="flex items-center gap-4">
|
|
54
|
+
{avatarSrc ? (
|
|
55
|
+
<img
|
|
56
|
+
src={avatarSrc}
|
|
57
|
+
alt=""
|
|
58
|
+
width="56"
|
|
59
|
+
height="56"
|
|
60
|
+
loading="lazy"
|
|
61
|
+
decoding="async"
|
|
62
|
+
class="h-12 w-12 shrink-0 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950 sm:h-14 sm:w-14"
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<span
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200 sm:h-14 sm:w-14"
|
|
68
|
+
>
|
|
69
|
+
{initials || "·"}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div class="min-w-0 flex-1">
|
|
74
|
+
<h1 class="m-0 text-[1.75rem] font-semibold leading-tight tracking-tight text-ink-900 dark:text-ink-100 sm:text-[2rem]">
|
|
75
|
+
{name}
|
|
76
|
+
</h1>
|
|
77
|
+
{ownerRepo && (
|
|
78
|
+
<p class="m-0 mt-0.5 text-sm text-ink-500 dark:text-ink-400">
|
|
79
|
+
<a
|
|
80
|
+
href={`https://github.com/${ownerRepo.owner}`}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
84
|
+
>
|
|
85
|
+
@{ownerRepo.owner}
|
|
86
|
+
</a>
|
|
87
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
88
|
+
<a
|
|
89
|
+
href={repoUrl}
|
|
90
|
+
target="_blank"
|
|
91
|
+
rel="noopener noreferrer"
|
|
92
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
93
|
+
>
|
|
94
|
+
{ownerRepo.repo}
|
|
95
|
+
</a>
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
67
99
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
100
|
+
{/* Pills — right-aligned beside the name on wide viewports. */}
|
|
101
|
+
<div class="hidden shrink-0 flex-wrap items-center justify-end gap-1.5 md:flex">
|
|
71
102
|
{category && (
|
|
72
103
|
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
73
104
|
{category}
|
|
@@ -78,8 +109,9 @@ const {
|
|
|
78
109
|
{l}
|
|
79
110
|
</span>
|
|
80
111
|
))}
|
|
112
|
+
{/* Curated = reviewed provenance, not a success status. */}
|
|
81
113
|
{record.curation?.reviewed && (
|
|
82
|
-
<span class="inline-flex items-center gap-1 rounded border border-
|
|
114
|
+
<span class="inline-flex items-center gap-1 rounded-full border border-border bg-secondary px-2 py-0.5 text-2xs font-medium text-secondary-foreground">
|
|
83
115
|
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
84
116
|
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z" />
|
|
85
117
|
</svg>
|
|
@@ -89,7 +121,7 @@ const {
|
|
|
89
121
|
{activityBadge && (
|
|
90
122
|
<span
|
|
91
123
|
data-activity-tone={activityBadge.tone}
|
|
92
|
-
class="grove-pill grove-pill--activity inline-flex items-center gap-1 rounded border px-
|
|
124
|
+
class="grove-pill grove-pill--activity inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-2xs font-medium"
|
|
93
125
|
>
|
|
94
126
|
<span class="relative flex h-1.5 w-1.5">
|
|
95
127
|
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-60"></span>
|
|
@@ -99,68 +131,65 @@ const {
|
|
|
99
131
|
</span>
|
|
100
132
|
)}
|
|
101
133
|
</div>
|
|
134
|
+
</div>
|
|
102
135
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
123
|
-
>
|
|
124
|
-
{ownerRepo.repo}
|
|
125
|
-
</a>
|
|
126
|
-
</p>
|
|
136
|
+
{/* Same pills below the identity row on narrow viewports. */}
|
|
137
|
+
<div class="mt-3 flex flex-wrap items-center gap-1.5 md:hidden">
|
|
138
|
+
{category && (
|
|
139
|
+
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
140
|
+
{category}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
{curationLabels.map((l) => (
|
|
144
|
+
<span class="rounded-full bg-secondary px-2 py-0.5 text-2xs font-medium text-secondary-foreground">
|
|
145
|
+
{l}
|
|
146
|
+
</span>
|
|
147
|
+
))}
|
|
148
|
+
{record.curation?.reviewed && (
|
|
149
|
+
<span class="inline-flex items-center gap-1 rounded-full border border-border bg-secondary px-2 py-0.5 text-2xs font-medium text-secondary-foreground">
|
|
150
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
151
|
+
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z" />
|
|
152
|
+
</svg>
|
|
153
|
+
Curated
|
|
154
|
+
</span>
|
|
127
155
|
)}
|
|
156
|
+
</div>
|
|
128
157
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
{/* Description — full width below the identity row. */}
|
|
159
|
+
{summary && (
|
|
160
|
+
<p class="m-0 mt-4 max-w-[72ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
161
|
+
{summary}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
{sourceDescription && sourceDescription !== summary && (
|
|
165
|
+
<p class="m-0 mt-2 max-w-[72ch] text-sm leading-relaxed text-ink-500 dark:text-ink-400">
|
|
166
|
+
<span class="text-2xs font-semibold uppercase tracking-wider">From the project's README: </span>
|
|
167
|
+
{sourceDescription}
|
|
168
|
+
</p>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* CTA row — primary actions only. */}
|
|
172
|
+
<div class="mt-4 flex flex-wrap items-center gap-2">
|
|
173
|
+
{repoUrl && (
|
|
174
|
+
<a
|
|
175
|
+
href={repoUrl}
|
|
176
|
+
target="_blank"
|
|
177
|
+
rel="noopener noreferrer"
|
|
178
|
+
class={buttonClass("primary", "sm")}
|
|
179
|
+
>
|
|
180
|
+
View on GitHub →
|
|
181
|
+
</a>
|
|
133
182
|
)}
|
|
134
|
-
{
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
183
|
+
{homepageUrl && (
|
|
184
|
+
<a
|
|
185
|
+
href={homepageUrl}
|
|
186
|
+
target="_blank"
|
|
187
|
+
rel="noopener noreferrer"
|
|
188
|
+
class={buttonClass("secondary", "sm")}
|
|
189
|
+
>
|
|
190
|
+
Homepage
|
|
191
|
+
</a>
|
|
139
192
|
)}
|
|
140
|
-
|
|
141
|
-
{/* CTA row — primary actions only. */}
|
|
142
|
-
<div class="mt-4 flex flex-wrap items-center gap-2">
|
|
143
|
-
{repoUrl && (
|
|
144
|
-
<a
|
|
145
|
-
href={repoUrl}
|
|
146
|
-
target="_blank"
|
|
147
|
-
rel="noopener noreferrer"
|
|
148
|
-
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"
|
|
149
|
-
>
|
|
150
|
-
View on GitHub →
|
|
151
|
-
</a>
|
|
152
|
-
)}
|
|
153
|
-
{homepageUrl && (
|
|
154
|
-
<a
|
|
155
|
-
href={homepageUrl}
|
|
156
|
-
target="_blank"
|
|
157
|
-
rel="noopener noreferrer"
|
|
158
|
-
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-border bg-secondary 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 text-foreground hover:bg-accent"
|
|
159
|
-
>
|
|
160
|
-
Homepage
|
|
161
|
-
</a>
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
193
|
</div>
|
|
165
194
|
{screenshots && screenshots.length > 0 && (
|
|
166
195
|
<div class="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3" aria-label="Screenshots">
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* RecordSection — generic lens-style section wrapper for the
|
|
4
4
|
* home page.
|
|
5
5
|
*
|
|
6
|
-
* Renders a SectionHeader + a 3-column grid of `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Renders a SectionHeader + a 3-column grid of `ProjectCard`s
|
|
7
|
+
* (the v0.5.0 refined card), plus an optional "View all" link
|
|
8
|
+
* in the header. Generic: takes any
|
|
9
9
|
* `IndexRecord[]` (the slim list-page shape, discriminated union
|
|
10
10
|
* over `kind`).
|
|
11
11
|
*
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* homepage never collapses to a blank section.
|
|
25
25
|
*/
|
|
26
26
|
import type { IndexRecord } from "@grove-dev/core";
|
|
27
|
-
import
|
|
27
|
+
import CardGrid from "./CardGrid.astro";
|
|
28
|
+
import ProjectCard from "./ProjectCard.astro";
|
|
28
29
|
import Container from "../layouts/Container.astro";
|
|
29
30
|
import SectionHeader from "../layouts/SectionHeader.astro";
|
|
30
31
|
|
|
@@ -47,6 +48,8 @@ interface Props {
|
|
|
47
48
|
emptyLabel?: string;
|
|
48
49
|
/** Extra class on the outer <section>. */
|
|
49
50
|
class?: string;
|
|
51
|
+
/** How many records to render. Default 3 — the refined grid shows one row. */
|
|
52
|
+
limit?: number;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const {
|
|
@@ -61,9 +64,10 @@ const {
|
|
|
61
64
|
eyebrow,
|
|
62
65
|
emptyLabel = "No records yet.",
|
|
63
66
|
class: className = "",
|
|
67
|
+
limit = 3,
|
|
64
68
|
} = Astro.props;
|
|
65
69
|
|
|
66
|
-
const visible = items.slice(0,
|
|
70
|
+
const visible = items.slice(0, limit);
|
|
67
71
|
---
|
|
68
72
|
|
|
69
73
|
<section
|
|
@@ -92,9 +96,7 @@ const visible = items.slice(0, 6);
|
|
|
92
96
|
{emptyLabel}
|
|
93
97
|
</p>
|
|
94
98
|
) : (
|
|
95
|
-
<
|
|
96
|
-
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
|
|
97
|
-
>
|
|
99
|
+
<CardGrid>
|
|
98
100
|
{visible.map((item) => {
|
|
99
101
|
// Resolve href: caller-supplied `hrefFor` callback wins;
|
|
100
102
|
// otherwise fall back to `${pathPrefix}/${slug}`. The href
|
|
@@ -106,10 +108,12 @@ const visible = items.slice(0, 6);
|
|
|
106
108
|
const finalHref =
|
|
107
109
|
typeof hrefFor === "function" ? hrefFor(item) : fallbackHref;
|
|
108
110
|
return (
|
|
109
|
-
<
|
|
111
|
+
<li class="m-0 p-0">
|
|
112
|
+
<ProjectCard record={item} href={finalHref} />
|
|
113
|
+
</li>
|
|
110
114
|
);
|
|
111
115
|
})}
|
|
112
|
-
</
|
|
116
|
+
</CardGrid>
|
|
113
117
|
)}
|
|
114
118
|
|
|
115
119
|
{viewAllHref && (
|