@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
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* the consumer's content pipeline). `emptyLabel` matches
|
|
14
14
|
* `RecordSection`'s API for empty-state copy.
|
|
15
15
|
*/
|
|
16
|
+
import { formatCount } from "@grove-dev/core";
|
|
17
|
+
import CardGrid from "./CardGrid.astro";
|
|
16
18
|
import CollectionRow from "./CollectionRow.astro";
|
|
17
19
|
import Container from "../layouts/Container.astro";
|
|
18
20
|
import SectionHeader from "../layouts/SectionHeader.astro";
|
|
21
|
+
import EmptyState from "../ui/EmptyState.astro";
|
|
19
22
|
import type { CollectionPageModel } from "../server/collections.js";
|
|
20
23
|
|
|
21
24
|
interface Props {
|
|
@@ -30,6 +33,13 @@ interface Props {
|
|
|
30
33
|
emptyLabel?: string;
|
|
31
34
|
/** Section id (deep link + stable key). */
|
|
32
35
|
id?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Heading level for the collection title. This component IS the
|
|
38
|
+
* page on collection detail routes, so it defaults to 1.
|
|
39
|
+
*/
|
|
40
|
+
headingLevel?: 1 | 2;
|
|
41
|
+
/** Noun pair for the entry count (blueprint labels). */
|
|
42
|
+
countNoun?: { singular: string; plural: string };
|
|
33
43
|
class?: string;
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -40,6 +50,8 @@ const {
|
|
|
40
50
|
description,
|
|
41
51
|
emptyLabel = "This collection is currently empty.",
|
|
42
52
|
id,
|
|
53
|
+
headingLevel = 1,
|
|
54
|
+
countNoun = { singular: "item", plural: "items" },
|
|
43
55
|
class: className = "",
|
|
44
56
|
} = Astro.props;
|
|
45
57
|
|
|
@@ -50,33 +62,44 @@ const finalEyebrow = eyebrow ?? (model.collection.kind === "curated" ? "Curated
|
|
|
50
62
|
|
|
51
63
|
<section id={id} class={`py-12 sm:py-16 ${className}`}>
|
|
52
64
|
<Container>
|
|
53
|
-
<SectionHeader eyebrow={finalEyebrow} title={finalTitle} description={finalDescription} />
|
|
65
|
+
<SectionHeader eyebrow={finalEyebrow} title={finalTitle} description={finalDescription} level={headingLevel} />
|
|
54
66
|
|
|
67
|
+
{/* Editorial context reads as a quiet callout, not metadata
|
|
68
|
+
shouting in uppercase. */}
|
|
55
69
|
{model.collection.selectionNote && (
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
<aside class="mb-6 max-w-[64ch] rounded-[var(--radius-lg)] border-l-2 border-border bg-surface-sunken px-4 py-3">
|
|
71
|
+
<p class="m-0 text-2xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
72
|
+
Selection criteria
|
|
73
|
+
</p>
|
|
74
|
+
<p class="m-0 mt-1 text-[0.875rem] leading-relaxed text-foreground">
|
|
75
|
+
{model.collection.selectionNote}
|
|
76
|
+
</p>
|
|
77
|
+
</aside>
|
|
60
78
|
)}
|
|
61
79
|
|
|
62
80
|
<slot name="intro" />
|
|
63
81
|
|
|
82
|
+
{/* Bridges the heading outline: the page title is an h1 and the
|
|
83
|
+
entry cards render h3 titles — without this h2 Lighthouse
|
|
84
|
+
flags a heading-order skip. */}
|
|
85
|
+
<h2 class="sr-only">Entries</h2>
|
|
64
86
|
<p class="m-0 mb-4 text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
65
|
-
{model.total
|
|
87
|
+
{formatCount(model.total, countNoun)}
|
|
66
88
|
</p>
|
|
67
89
|
|
|
68
90
|
{model.isEmpty ? (
|
|
69
|
-
<
|
|
70
|
-
{emptyLabel}
|
|
71
|
-
|
|
91
|
+
<EmptyState
|
|
92
|
+
title={emptyLabel}
|
|
93
|
+
description="No records currently match this collection's query. The collection fills automatically as matching records are added."
|
|
94
|
+
/>
|
|
72
95
|
) : (
|
|
73
|
-
<
|
|
96
|
+
<CardGrid>
|
|
74
97
|
{model.entries.map((entry, i) => (
|
|
75
98
|
<li class="m-0 p-0">
|
|
76
|
-
<CollectionRow entry={entry} eager={i <
|
|
99
|
+
<CollectionRow entry={entry} eager={i < 3} />
|
|
77
100
|
</li>
|
|
78
101
|
))}
|
|
79
|
-
</
|
|
102
|
+
</CardGrid>
|
|
80
103
|
)}
|
|
81
104
|
|
|
82
105
|
{model.related.length > 0 && (
|
|
@@ -85,7 +108,7 @@ const finalEyebrow = eyebrow ?? (model.collection.kind === "curated" ? "Curated
|
|
|
85
108
|
eyebrow="Related"
|
|
86
109
|
title="Related collections"
|
|
87
110
|
description="Other curated groupings of related items."
|
|
88
|
-
level={
|
|
111
|
+
level={2}
|
|
89
112
|
/>
|
|
90
113
|
<ul class="m-0 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-2 lg:grid-cols-4">
|
|
91
114
|
{model.related.map((r) => (
|
|
@@ -1,38 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* CollectionRow —
|
|
4
|
-
*
|
|
3
|
+
* CollectionRow — collection-page adapter around the canonical
|
|
4
|
+
* `ProjectCard`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (which carries `kind`, `repoUrl`, `github`, `bestFor` etc.) to
|
|
11
|
-
* accept a `CollectionEntry` cleanly.
|
|
6
|
+
* Consumes the lightweight `CollectionEntry` shape directly and maps
|
|
7
|
+
* it onto `ProjectCard`'s explicit props, so a collection page sitting
|
|
8
|
+
* next to the directory index reads as the same site. The historical
|
|
9
|
+
* public props are preserved:
|
|
12
10
|
*
|
|
13
|
-
* API parity with `ItemCard`:
|
|
14
11
|
* - `entry` — the collection entry (slug, title, description, …)
|
|
15
12
|
* - `href` — full detail URL; wins over `routeSlug`
|
|
16
13
|
* - `routeSlug` — blueprint slug (e.g. "projects"); details derived
|
|
17
14
|
* as `/${routeSlug}/${slug}` when `href` is not provided
|
|
18
15
|
* - `showChips` — render platform + stack chip row (default true)
|
|
19
|
-
* - `showMeta` — render stars/updated/license metadata
|
|
16
|
+
* - `showMeta` — render stars/updated/license metadata (default true)
|
|
20
17
|
* - `showLabel` — render the curated/category label (default true)
|
|
21
|
-
* - `class` — extra class on the
|
|
22
|
-
* - `eager` — bypass lazy loading for above-the-fold avatars
|
|
18
|
+
* - `class` — extra class on the card root
|
|
19
|
+
* - `eager` — bypass lazy loading for above-the-fold avatars
|
|
23
20
|
*
|
|
24
21
|
* Footer links:
|
|
25
22
|
* - "View repo" — when `entry.repoHref` is set
|
|
26
23
|
* - "Visit site" — when `entry.homepageHref` is set
|
|
27
|
-
* - "Details →" — always rendered (links to the consumer's detail page)
|
|
28
24
|
*
|
|
29
|
-
* Analytics: emits `data-event="collection_item_click"`
|
|
30
|
-
* `data-event-source="collection_row"`
|
|
31
|
-
* in BaseLayout.
|
|
25
|
+
* Analytics: emits `data-event="collection_item_click"` /
|
|
26
|
+
* `data-event-source="collection_row"` on the card root and per-link
|
|
27
|
+
* events on the footer links, so sites wire one handler in BaseLayout.
|
|
32
28
|
*/
|
|
33
29
|
import type { CollectionEntry } from "@grove-dev/core";
|
|
34
|
-
import
|
|
35
|
-
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
30
|
+
import ProjectCard from "./ProjectCard.astro";
|
|
36
31
|
|
|
37
32
|
interface Props {
|
|
38
33
|
entry: CollectionEntry;
|
|
@@ -65,182 +60,57 @@ const detailHref =
|
|
|
65
60
|
? hrefProp
|
|
66
61
|
: entry.url || (base ? `/${base}/${slug}` : `/${slug}`);
|
|
67
62
|
|
|
68
|
-
const title = entry.title;
|
|
69
|
-
const initials = title
|
|
70
|
-
.split(/\s+/)
|
|
71
|
-
.map((w) => w[0])
|
|
72
|
-
.filter(Boolean)
|
|
73
|
-
.slice(0, 2)
|
|
74
|
-
.join("")
|
|
75
|
-
.toUpperCase();
|
|
76
|
-
|
|
77
|
-
// Derive a logo URL from the entry's `repoHref` when present;
|
|
78
|
-
// otherwise fall back to the detail URL. Detail URL is the
|
|
79
|
-
// consumer's page, not necessarily a GitHub owner — so the
|
|
80
|
-
// initials placeholder is the safe default when nothing matches.
|
|
81
63
|
const repoHref = entry.repoHref;
|
|
82
|
-
const
|
|
83
|
-
const avatar = owner ? getOwnerAvatarUrl(owner, 80) : undefined;
|
|
84
|
-
|
|
85
|
-
const stack = entry.stack ? [entry.stack] : [];
|
|
86
|
-
const platform = entry.platform?.slice(0, 4) ?? [];
|
|
87
|
-
const platformOverflow = (entry.platform?.length ?? 0) - platform.length;
|
|
88
|
-
|
|
89
|
-
const stars = entry.stars;
|
|
90
|
-
const starsLabel = formatStars(stars);
|
|
91
|
-
const updatedAt = entry.pushedAt;
|
|
92
|
-
const updatedLabel = formatRelative(updatedAt);
|
|
93
|
-
const license = entry.license ?? null;
|
|
94
|
-
const status = entry.status;
|
|
95
|
-
const statusText = status ? statusDisplay(status as Parameters<typeof statusDisplay>[0]) : "";
|
|
96
|
-
const category = entry.categories?.[0];
|
|
64
|
+
const homepageHref = entry.homepageHref;
|
|
97
65
|
const curated = Boolean(entry.curationScore || (entry.activityScore && entry.activityScore > 0.5));
|
|
98
66
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
67
|
+
const secondaryLinks = [
|
|
68
|
+
...(repoHref
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
label: "View repo",
|
|
72
|
+
href: repoHref,
|
|
73
|
+
external: true,
|
|
74
|
+
event: "external_repo_click",
|
|
75
|
+
eventSource: "collection_row_view_repo",
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
...(homepageHref
|
|
80
|
+
? [
|
|
81
|
+
{
|
|
82
|
+
label: "Visit site",
|
|
83
|
+
href: homepageHref,
|
|
84
|
+
external: true,
|
|
85
|
+
event: "external_link_click",
|
|
86
|
+
eventSource: "collection_row_visit_site",
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
: []),
|
|
90
|
+
];
|
|
102
91
|
---
|
|
103
92
|
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</span>
|
|
129
|
-
)}
|
|
130
|
-
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
131
|
-
<h3 class="m-0 truncate text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
132
|
-
<a href={detailHref} class="inline-flex min-h-6 min-w-6 items-center text-inherit no-underline">
|
|
133
|
-
{title}
|
|
134
|
-
</a>
|
|
135
|
-
</h3>
|
|
136
|
-
{showLabel && (
|
|
137
|
-
<span class="truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
138
|
-
{category}
|
|
139
|
-
{status && (
|
|
140
|
-
<>
|
|
141
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
142
|
-
<span class="text-ink-500 dark:text-ink-400">{statusText}</span>
|
|
143
|
-
</>
|
|
144
|
-
)}
|
|
145
|
-
</span>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
{curated && showLabel && (
|
|
149
|
-
<span
|
|
150
|
-
class="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
|
|
151
|
-
title="Has curation or activity score"
|
|
152
|
-
>
|
|
153
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
154
|
-
<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" />
|
|
155
|
-
</svg>
|
|
156
|
-
curated
|
|
157
|
-
</span>
|
|
158
|
-
)}
|
|
159
|
-
</header>
|
|
160
|
-
|
|
161
|
-
{entry.description && (
|
|
162
|
-
<p class="m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
163
|
-
{entry.description}
|
|
164
|
-
</p>
|
|
165
|
-
)}
|
|
166
|
-
|
|
167
|
-
{/* Stack + platform rendered as 2 distinct labelled rows so the
|
|
168
|
-
primary/secondary hierarchy is unambiguous. Stack is the more
|
|
169
|
-
identifying signal for an item (it's the main technology) and
|
|
170
|
-
gets a filled pill; platform is secondary and gets an outlined
|
|
171
|
-
pill. Shared with `IndexRow` / `ItemCard` so the pattern is
|
|
172
|
-
identical across all card-shaped components. */}
|
|
173
|
-
<StackPlatformChips
|
|
174
|
-
show={showChips}
|
|
175
|
-
stack={stack[0]}
|
|
176
|
-
platform={platform}
|
|
177
|
-
/>
|
|
178
|
-
|
|
179
|
-
{showMeta && (
|
|
180
|
-
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
181
|
-
{starsLabel !== null && (
|
|
182
|
-
<span class="inline-flex items-center gap-1" title={`${stars ?? 0} stars`}>
|
|
183
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
184
|
-
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
|
|
185
|
-
</svg>
|
|
186
|
-
<span class="font-mono">{starsLabel}</span>
|
|
187
|
-
</span>
|
|
188
|
-
)}
|
|
189
|
-
{updatedAt && (
|
|
190
|
-
<span class="inline-flex items-center gap-1" title={`Last updated ${updatedAt}`}>
|
|
191
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-ink-400">
|
|
192
|
-
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm0 1.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm-.5 1.5a.5.5 0 0 0-1 0v3.25c0 .15.07.29.18.39l1.95 1.65a.5.5 0 0 0 .66-.76L7.5 7.55V5Z" />
|
|
193
|
-
</svg>
|
|
194
|
-
<span class="font-mono">Updated {updatedLabel}</span>
|
|
195
|
-
</span>
|
|
196
|
-
)}
|
|
197
|
-
{license && (
|
|
198
|
-
<span class="inline-flex items-center gap-1" title={`License: ${license}`}>
|
|
199
|
-
<span class="font-mono uppercase tracking-wide text-[10px]">{license}</span>
|
|
200
|
-
</span>
|
|
201
|
-
)}
|
|
202
|
-
</div>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{/* Footer: source links (View repo / Visit site) + Details. */}
|
|
206
|
-
<footer class="mt-1 flex items-center justify-between gap-3 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
207
|
-
<div class="flex min-w-0 items-center gap-3">
|
|
208
|
-
{showViewRepo && (
|
|
209
|
-
<a
|
|
210
|
-
href={repoHref}
|
|
211
|
-
target="_blank"
|
|
212
|
-
rel="noopener noreferrer"
|
|
213
|
-
data-event="external_repo_click"
|
|
214
|
-
data-event-source="collection_row_view_repo"
|
|
215
|
-
data-event-item={slug}
|
|
216
|
-
class="inline-flex items-center gap-1 font-medium text-ink-700 transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
217
|
-
>
|
|
218
|
-
View repo
|
|
219
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
220
|
-
<path d="M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Z" />
|
|
221
|
-
<path d="M10 0a.75.75 0 0 0-.75.75v.75h-3a.75.75 0 0 0 0 1.5h3v.75A.75.75 0 0 0 10.75 4.5h2.5A.75.75 0 0 0 14 3.75v-2.5A.75.75 0 0 0 13.25.5H10Z" />
|
|
222
|
-
</svg>
|
|
223
|
-
</a>
|
|
224
|
-
)}
|
|
225
|
-
{showVisitSite && (
|
|
226
|
-
<a
|
|
227
|
-
href={homepageHref}
|
|
228
|
-
target="_blank"
|
|
229
|
-
rel="noopener noreferrer"
|
|
230
|
-
data-event="external_link_click"
|
|
231
|
-
data-event-source="collection_row_visit_site"
|
|
232
|
-
data-event-item={slug}
|
|
233
|
-
class="inline-flex items-center gap-1 font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
234
|
-
>
|
|
235
|
-
Visit site →
|
|
236
|
-
</a>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
<a
|
|
240
|
-
href={detailHref}
|
|
241
|
-
class="inline-flex shrink-0 items-center gap-1 font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
242
|
-
>
|
|
243
|
-
Details →
|
|
244
|
-
</a>
|
|
245
|
-
</footer>
|
|
246
|
-
</article>
|
|
93
|
+
<ProjectCard
|
|
94
|
+
href={detailHref}
|
|
95
|
+
slug={slug}
|
|
96
|
+
name={entry.title}
|
|
97
|
+
description={entry.description ?? ""}
|
|
98
|
+
repoUrl={repoHref}
|
|
99
|
+
stars={showMeta ? entry.stars : undefined}
|
|
100
|
+
pushedAt={showMeta ? entry.pushedAt : undefined}
|
|
101
|
+
license={showMeta ? (entry.license ?? null) : null}
|
|
102
|
+
category={entry.categories?.[0]}
|
|
103
|
+
status={entry.status}
|
|
104
|
+
curated={curated}
|
|
105
|
+
stack={entry.stack}
|
|
106
|
+
platforms={entry.platform ?? []}
|
|
107
|
+
showCurated={showLabel}
|
|
108
|
+
showLicense={showMeta}
|
|
109
|
+
showCategoryStatus={showLabel}
|
|
110
|
+
labelledChips={showChips}
|
|
111
|
+
secondaryLinks={secondaryLinks}
|
|
112
|
+
eager={eager}
|
|
113
|
+
event="collection_item_click"
|
|
114
|
+
eventSource="collection_row"
|
|
115
|
+
class={className}
|
|
116
|
+
/>
|
|
@@ -18,6 +18,8 @@ interface Props {
|
|
|
18
18
|
viewAllLabel?: string;
|
|
19
19
|
/** Cap the number of cards rendered. Default: 3. */
|
|
20
20
|
limit?: number;
|
|
21
|
+
/** Noun pair for the per-card entry counts (blueprint labels). */
|
|
22
|
+
countNoun?: { singular: string; plural: string };
|
|
21
23
|
class?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -28,6 +30,7 @@ const {
|
|
|
28
30
|
viewAllHref = "/collections/",
|
|
29
31
|
viewAllLabel = "View all collections",
|
|
30
32
|
limit = 3,
|
|
33
|
+
countNoun,
|
|
31
34
|
class: className,
|
|
32
35
|
} = Astro.props;
|
|
33
36
|
---
|
|
@@ -39,5 +42,6 @@ const {
|
|
|
39
42
|
viewAllHref={viewAllHref}
|
|
40
43
|
viewAllLabel={viewAllLabel}
|
|
41
44
|
limit={limit}
|
|
45
|
+
countNoun={countNoun}
|
|
42
46
|
class={className}
|
|
43
47
|
/>
|
|
@@ -28,6 +28,8 @@ interface Props {
|
|
|
28
28
|
viewAllLabel?: string;
|
|
29
29
|
title?: string;
|
|
30
30
|
description?: string;
|
|
31
|
+
/** Render per-user contribution counts (config: contributors.showContributionCount). */
|
|
32
|
+
showContributionCount?: boolean;
|
|
31
33
|
class?: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -38,6 +40,7 @@ const {
|
|
|
38
40
|
viewAllLabel = "View all",
|
|
39
41
|
title = "Built by the community",
|
|
40
42
|
description = "The people improving this directory through code, curation, and review.",
|
|
43
|
+
showContributionCount = true,
|
|
41
44
|
class: className = "",
|
|
42
45
|
} = Astro.props;
|
|
43
46
|
|
|
@@ -125,7 +128,7 @@ const visible = contributors
|
|
|
125
128
|
<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
129
|
{c.name ?? c.username}
|
|
127
130
|
</span>
|
|
128
|
-
{typeof c.contributions === "number" && (
|
|
131
|
+
{showContributionCount && typeof c.contributions === "number" && (
|
|
129
132
|
<span class="font-mono text-2xs text-ink-400">
|
|
130
133
|
{fmt(c.contributions) ?? c.contributions}
|
|
131
134
|
</span>
|