@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.
Files changed (72) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/index.d.ts +0 -2
  4. package/dist/lib/index.d.ts.map +1 -1
  5. package/dist/lib/index.js +0 -2
  6. package/dist/lib/index.js.map +1 -1
  7. package/dist/ui/button.d.ts +45 -0
  8. package/dist/ui/button.d.ts.map +1 -0
  9. package/dist/ui/button.js +82 -0
  10. package/dist/ui/button.js.map +1 -0
  11. package/package.json +8 -5
  12. package/src/components/CardGrid.astro +41 -0
  13. package/src/components/CardIcon.astro +67 -0
  14. package/src/components/CategoryGrid.astro +20 -5
  15. package/src/components/CollectionCard.astro +56 -0
  16. package/src/components/CollectionIndex.astro +21 -26
  17. package/src/components/CollectionPage.astro +36 -13
  18. package/src/components/CollectionRow.astro +62 -192
  19. package/src/components/CollectionTeaser.astro +4 -0
  20. package/src/components/ContributorsGrid.astro +4 -1
  21. package/src/components/DirectoryIndexClient.astro +127 -30
  22. package/src/components/EditorialSummary.astro +6 -5
  23. package/src/components/FilterGroupMenu.astro +24 -10
  24. package/src/components/FilterOptions.astro +6 -1
  25. package/src/components/FinalCta.astro +5 -3
  26. package/src/components/Hero.astro +85 -139
  27. package/src/components/IndexRow.astro +36 -69
  28. package/src/components/LanguageBreakdown.astro +5 -1
  29. package/src/components/OriginalCollection.astro +3 -2
  30. package/src/components/Pagination.astro +7 -20
  31. package/src/components/ProjectCard.astro +345 -0
  32. package/src/components/RecordHeader.astro +111 -82
  33. package/src/components/RecordSection.astro +14 -10
  34. package/src/components/RecordSidebar.astro +68 -20
  35. package/src/components/RefinePanel.astro +41 -62
  36. package/src/components/SmartLensTabs.astro +3 -7
  37. package/src/components/StackGrid.astro +26 -6
  38. package/src/components/SubmissionClient.astro +140 -63
  39. package/src/components/TableOfContents.astro +31 -5
  40. package/src/components/WhyThisExists.astro +1 -1
  41. package/src/index.ts +2 -2
  42. package/src/layouts/BaseLayout.astro +79 -9
  43. package/src/layouts/Header.astro +5 -4
  44. package/src/layouts/SectionHeader.astro +3 -2
  45. package/src/layouts/Seo.astro +28 -17
  46. package/src/layouts/ThemeToggle.astro +23 -6
  47. package/src/lib/index.ts +0 -2
  48. package/src/server/collections.ts +11 -0
  49. package/src/server/contrast.test.ts +82 -0
  50. package/src/server/contrast.ts +117 -0
  51. package/src/server/directory.test.ts +177 -0
  52. package/src/server/directory.ts +328 -206
  53. package/src/server/github-repo.test.ts +88 -0
  54. package/src/server/github-repo.ts +104 -0
  55. package/src/server/index.ts +4 -3
  56. package/src/server/models-home.test.ts +101 -0
  57. package/src/server/models.test.ts +135 -0
  58. package/src/server/models.ts +170 -42
  59. package/src/styles.css +128 -16
  60. package/dist/lib/scores.d.ts +0 -2
  61. package/dist/lib/scores.d.ts.map +0 -1
  62. package/dist/lib/scores.js +0 -2
  63. package/dist/lib/scores.js.map +0 -1
  64. package/src/components/CurationGrid.astro +0 -41
  65. package/src/components/DecisionRow.astro +0 -89
  66. package/src/components/ExploreByCategory.astro +0 -67
  67. package/src/components/ExploreByStack.astro +0 -78
  68. package/src/components/GroveDocumentHead.astro +0 -28
  69. package/src/components/ItemCard.astro +0 -324
  70. package/src/components/MinimalAbout.astro +0 -82
  71. package/src/components/ScoreBars.astro +0 -102
  72. 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
- <p class="m-0 mb-6 max-w-[64ch] text-[0.875rem] font-medium text-ink-700 dark:text-ink-300">
57
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Selection criteria · </span>
58
- {model.collection.selectionNote}
59
- </p>
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} {model.total === 1 ? "item" : "items"}
87
+ {formatCount(model.total, countNoun)}
66
88
  </p>
67
89
 
68
90
  {model.isEmpty ? (
69
- <p class="rounded-md border border-dashed border-ink-200 bg-white p-6 text-sm text-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-400">
70
- {emptyLabel}
71
- </p>
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
- <ul class="m-0 flex list-none flex-col gap-3 p-0">
96
+ <CardGrid>
74
97
  {model.entries.map((entry, i) => (
75
98
  <li class="m-0 p-0">
76
- <CollectionRow entry={entry} eager={i < 2} />
99
+ <CollectionRow entry={entry} eager={i < 3} />
77
100
  </li>
78
101
  ))}
79
- </ul>
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={3}
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 — card-shaped row for a single entry inside a
4
- * curated/generated collection.
3
+ * CollectionRow — collection-page adapter around the canonical
4
+ * `ProjectCard`.
5
5
  *
6
- * Mirrors `ItemCard`'s visual treatment so a collection page sitting
7
- * next to the directory index reads as the same site. The component
8
- * consumes the lightweight `CollectionEntry` shape directly the
9
- * existing `IndexRow` is too tightly coupled to `IndexRecord`
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 row (default true)
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 outer <article>
22
- * - `eager` — bypass lazy loading for above-the-fold avatars (first 1-2)
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"` and
30
- * `data-event-source="collection_row"` so sites wire one handler
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 { formatRelative, formatStars, statusDisplay, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
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 { owner } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
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 homepageHref = entry.homepageHref;
100
- const showViewRepo = typeof repoHref === "string" && repoHref.length > 0;
101
- const showVisitSite = typeof homepageHref === "string" && homepageHref.length > 0;
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
- <article
105
- data-event="collection_item_click"
106
- data-event-source="collection_row"
107
- data-event-item={slug}
108
- class={`rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors hover:bg-secondary group relative flex h-full flex-col gap-3 p-4 ${className}`}
109
- >
110
- <header class="flex items-start gap-3">
111
- {avatar ? (
112
- <img
113
- src={avatar}
114
- alt={`${title} avatar`}
115
- width="40"
116
- height="40"
117
- loading={eager ? "eager" : "lazy"}
118
- fetchpriority={eager ? "high" : undefined}
119
- decoding="async"
120
- class="h-10 w-10 shrink-0 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
121
- />
122
- ) : (
123
- <span
124
- aria-hidden="true"
125
- class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-2xs font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
126
- >
127
- {initials}
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>