@grove-dev/astro 0.7.0 → 0.8.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 (110) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +22 -33
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.d.ts +1 -1
  6. package/dist/lib/display.js +1 -1
  7. package/dist/lib/format.d.ts +1 -1
  8. package/dist/lib/format.js +1 -1
  9. package/dist/lib/icon-kinds.d.ts +1 -1
  10. package/dist/lib/icon-kinds.js +43 -43
  11. package/dist/lib/icon-registry.d.ts +2 -2
  12. package/dist/lib/icon-registry.js +23 -23
  13. package/dist/lib/index.d.ts +9 -9
  14. package/dist/lib/index.d.ts.map +1 -1
  15. package/dist/lib/index.js +9 -9
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/lenses.d.ts +1 -1
  18. package/dist/lib/lenses.js +1 -1
  19. package/dist/lib/packaged-icons.d.ts +1 -1
  20. package/dist/lib/packaged-icons.js +4 -4
  21. package/dist/lib/repo.d.ts +1 -1
  22. package/dist/lib/repo.js +1 -1
  23. package/dist/lib/search.d.ts +1 -1
  24. package/dist/lib/search.js +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +1 -1
  26. package/dist/lib/taxonomy-counts.js +1 -1
  27. package/package.json +7 -17
  28. package/src/index.ts +23 -38
  29. package/src/lib/display.ts +1 -1
  30. package/src/lib/format.ts +1 -1
  31. package/src/lib/icon-kinds.ts +44 -44
  32. package/src/lib/icon-registry.ts +25 -25
  33. package/src/lib/index.ts +9 -9
  34. package/src/lib/lenses.ts +1 -1
  35. package/src/lib/packaged-icons.ts +8 -8
  36. package/src/lib/repo.ts +1 -1
  37. package/src/lib/search.ts +1 -1
  38. package/src/lib/taxonomy-counts.ts +1 -1
  39. package/src/server/collections.ts +53 -42
  40. package/src/server/contrast.ts +14 -9
  41. package/src/server/directory.ts +22 -17
  42. package/src/server/index.ts +3 -2
  43. package/src/server/models.ts +404 -213
  44. package/src/server/seo.ts +34 -31
  45. package/src/styles.css +235 -91
  46. package/dist/layouts/index.d.ts +0 -25
  47. package/dist/layouts/index.d.ts.map +0 -1
  48. package/dist/layouts/index.js +0 -25
  49. package/dist/layouts/index.js.map +0 -1
  50. package/dist/ui/button.d.ts +0 -53
  51. package/dist/ui/button.d.ts.map +0 -1
  52. package/dist/ui/button.js +0 -90
  53. package/dist/ui/button.js.map +0 -1
  54. package/src/components/CardGrid.astro +0 -41
  55. package/src/components/CardIcon.astro +0 -67
  56. package/src/components/CategoryGrid.astro +0 -114
  57. package/src/components/CollectionCard.astro +0 -56
  58. package/src/components/CollectionIndex.astro +0 -102
  59. package/src/components/CollectionPage.astro +0 -128
  60. package/src/components/CollectionRow.astro +0 -81
  61. package/src/components/CollectionTeaser.astro +0 -47
  62. package/src/components/ContributorsGrid.astro +0 -143
  63. package/src/components/DirectoryIndexClient.astro +0 -510
  64. package/src/components/EditorialSummary.astro +0 -58
  65. package/src/components/FilterGroupMenu.astro +0 -69
  66. package/src/components/FilterOptions.astro +0 -49
  67. package/src/components/FinalCta.astro +0 -43
  68. package/src/components/Hero.astro +0 -243
  69. package/src/components/Icon.astro +0 -191
  70. package/src/components/IndexRow.astro +0 -37
  71. package/src/components/LanguageBreakdown.astro +0 -67
  72. package/src/components/MarkdownBody.astro +0 -37
  73. package/src/components/OriginalCollection.astro +0 -117
  74. package/src/components/Pagination.astro +0 -50
  75. package/src/components/PoweredBy.astro +0 -60
  76. package/src/components/ProjectCard.astro +0 -321
  77. package/src/components/RecordHeader.astro +0 -221
  78. package/src/components/RecordSection.astro +0 -128
  79. package/src/components/RecordSidebar.astro +0 -387
  80. package/src/components/RefinePanel.astro +0 -168
  81. package/src/components/SmartLensTabs.astro +0 -81
  82. package/src/components/StackGrid.astro +0 -151
  83. package/src/components/StackPlatformChips.astro +0 -73
  84. package/src/components/SubmissionClient.astro +0 -254
  85. package/src/components/TableOfContents.astro +0 -140
  86. package/src/components/WhyThisExists.astro +0 -116
  87. package/src/layouts/BaseLayout.astro +0 -415
  88. package/src/layouts/Container.astro +0 -18
  89. package/src/layouts/Footer.astro +0 -231
  90. package/src/layouts/Header.astro +0 -214
  91. package/src/layouts/SectionHeader.astro +0 -43
  92. package/src/layouts/Seo.astro +0 -171
  93. package/src/layouts/ThemeToggle.astro +0 -118
  94. package/src/layouts/astro-shim.d.ts +0 -29
  95. package/src/layouts/index.ts +0 -24
  96. package/src/server/collections.test.ts +0 -212
  97. package/src/server/contrast.test.ts +0 -82
  98. package/src/server/directory.test.ts +0 -177
  99. package/src/server/github-repo.test.ts +0 -88
  100. package/src/server/models-home.test.ts +0 -101
  101. package/src/server/models.test.ts +0 -135
  102. package/src/server/seo.test.ts +0 -123
  103. package/src/ui/Badge.astro +0 -38
  104. package/src/ui/Button.astro +0 -42
  105. package/src/ui/EmptyState.astro +0 -38
  106. package/src/ui/FilterDrawer.astro +0 -124
  107. package/src/ui/PageHeader.astro +0 -39
  108. package/src/ui/SearchField.astro +0 -106
  109. package/src/ui/button.test.ts +0 -54
  110. package/src/ui/button.ts +0 -107
@@ -1,117 +0,0 @@
1
- ---
2
- /**
3
- * OriginalCollection — legacy lineage card.
4
- *
5
- * Renders a single wide card that links out to the project's origin
6
- * repository, with stars/forks/contributors stats. Used on home and
7
- * about pages to give visitors a way to find the original curated
8
- * collection this directory descends from.
9
- *
10
- * Generic: takes plain `stats` and a `repoUrl`. No blueprint or
11
- * site-specific copy.
12
- */
13
- import { buttonClass } from "../ui/button.js";
14
-
15
- interface OriginStats {
16
- stars?: number;
17
- forks?: number;
18
- contributors?: number;
19
- description?: string;
20
- }
21
-
22
- interface Props {
23
- repoUrl: string;
24
- repoLabel?: string;
25
- stats?: OriginStats;
26
- title?: string;
27
- description?: string;
28
- class?: string;
29
- }
30
-
31
- const {
32
- repoUrl,
33
- repoLabel = "View on GitHub",
34
- stats = {},
35
- title = "A growing list, forked from the original",
36
- description = "This directory descends from an earlier curated collection. The original still lives on — same shape, same spirit, fresh data.",
37
- class: className = "",
38
- } = Astro.props;
39
-
40
- function fmt(n: number | undefined): string | null {
41
- if (typeof n !== "number" || !Number.isFinite(n)) return null;
42
- if (n >= 1000) {
43
- const k = n / 1000;
44
- return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
45
- }
46
- return n.toString();
47
- }
48
-
49
- const hasStats =
50
- stats.stars !== undefined ||
51
- stats.forks !== undefined ||
52
- stats.contributors !== undefined;
53
- ---
54
-
55
- <section class:list={["py-12 sm:py-16", className]}>
56
- <div class="mx-auto w-full max-w-container px-5 sm:px-7">
57
- <div
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"
59
- >
60
- <div class="flex flex-col gap-2">
61
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
62
- Origin
63
- </span>
64
- <h2 class="m-0 text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
65
- {title}
66
- </h2>
67
- <p class="m-0 max-w-2xl text-[0.875rem] text-ink-500 dark:text-ink-400">
68
- {description}
69
- </p>
70
- {stats.description && (
71
- <p class="m-0 max-w-2xl text-[0.8125rem] text-ink-500 dark:text-ink-400">
72
- {stats.description}
73
- </p>
74
- )}
75
- {hasStats && (
76
- <dl
77
- class="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-2xs text-ink-500 dark:text-ink-400"
78
- aria-label="Original repository signals"
79
- >
80
- {stats.stars !== undefined && (
81
- <div class="flex items-center gap-1">
82
- <dd class="m-0 font-mono">{fmt(stats.stars) ?? stats.stars}</dd>
83
- <dt>stars</dt>
84
- </div>
85
- )}
86
- {stats.forks !== undefined && (
87
- <div class="flex items-center gap-1">
88
- <dd class="m-0 font-mono">{fmt(stats.forks) ?? stats.forks}</dd>
89
- <dt>forks</dt>
90
- </div>
91
- )}
92
- {stats.contributors !== undefined && (
93
- <div class="flex items-center gap-1">
94
- <dd class="m-0 font-mono">
95
- {fmt(stats.contributors) ?? stats.contributors}
96
- </dd>
97
- <dt>contributors</dt>
98
- </div>
99
- )}
100
- </dl>
101
- )}
102
- </div>
103
- <div class="flex flex-shrink-0 items-center gap-2">
104
- <a
105
- href={repoUrl}
106
- target="_blank"
107
- rel="noopener noreferrer"
108
- class={buttonClass("primary", "sm")}
109
- data-event="original_collection_click"
110
- data-event-source="original_collection"
111
- >
112
- {repoLabel} →
113
- </a>
114
- </div>
115
- </div>
116
- </div>
117
- </section>
@@ -1,50 +0,0 @@
1
- ---
2
- interface Props {
3
- current: number;
4
- total: number;
5
- hrefFor: (page: number) => string;
6
- class?: string;
7
- }
8
-
9
- import { paginationPageList } from "@grove-dev/astro";
10
- import { buttonClass, PAGINATION_DISABLED, PAGINATION_EXTRA } from "../ui/button.js";
11
-
12
- const { current, total, hrefFor, class: className = "" } = Astro.props;
13
-
14
- const pages = paginationPageList(current, total);
15
- const linkClass = buttonClass("secondary", "sm", PAGINATION_EXTRA);
16
- const currentClass = buttonClass("selected", "sm", PAGINATION_EXTRA);
17
- ---
18
-
19
- {total > 1 && (
20
- <nav
21
- class:list={["flex flex-wrap items-center justify-center gap-1.5", className]}
22
- aria-label="Pagination"
23
- >
24
- {current > 1 ? (
25
- <a href={hrefFor(current - 1)} class={linkClass} rel="prev">Previous</a>
26
- ) : (
27
- <span class={`${linkClass} ${PAGINATION_DISABLED}`}>Previous</span>
28
- )}
29
-
30
- {pages.map((page) =>
31
- page === "ellipsis" ? (
32
- <span class="px-1 text-2xs text-ink-400" aria-hidden="true">...</span>
33
- ) : (
34
- <a
35
- href={hrefFor(page)}
36
- class={page === current ? currentClass : linkClass}
37
- aria-current={page === current ? "page" : undefined}
38
- >
39
- {page}
40
- </a>
41
- ),
42
- )}
43
-
44
- {current < total ? (
45
- <a href={hrefFor(current + 1)} class={linkClass} rel="next">Next</a>
46
- ) : (
47
- <span class={`${linkClass} ${PAGINATION_DISABLED}`}>Next</span>
48
- )}
49
- </nav>
50
- )}
@@ -1,60 +0,0 @@
1
- ---
2
- /**
3
- * PoweredBy — "Powered by Grove" attribution.
4
- *
5
- * The mark is **inlined**, not loaded through `<img src>`: an SVG
6
- * served as its own document cannot see page CSS, so `currentColor`
7
- * never resolves there (the bug that cost the packaged icon set both
8
- * themes before 0.5.3). Inlined, the mark simply inherits the
9
- * surrounding text colour and follows the site's theme toggle — no
10
- * second file, no script.
11
- *
12
- * Generic: no `siteConfig` import. `Footer` renders it under the brand
13
- * block when `footer.poweredBy` is on; consumers can also drop it
14
- * anywhere themselves — e.g. into `Hero`'s `eyebrow` slot.
15
- */
16
-
17
- interface Props {
18
- /** Where the link points. Override only for a staging/docs mirror. */
19
- href?: string;
20
- /** Mark size in px. Default 16 — sized for 13px text. */
21
- size?: number;
22
- /** Extra class on the link. */
23
- class?: string;
24
- }
25
-
26
- const {
27
- href = "https://withgrove.dev",
28
- size = 16,
29
- class: className = "",
30
- } = Astro.props;
31
- ---
32
-
33
- <a
34
- href={href}
35
- target="_blank"
36
- rel="noopener noreferrer"
37
- class:list={[
38
- "inline-flex items-center gap-1.5 transition-colors hover:text-ink-900 dark:hover:text-ink-100",
39
- className,
40
- ]}
41
- >
42
- <span>Powered by</span>
43
- <svg
44
- xmlns="http://www.w3.org/2000/svg"
45
- viewBox="0 0 32 32"
46
- width={size}
47
- height={size}
48
- style={`width:${size}px;height:${size}px`}
49
- fill="none"
50
- aria-hidden="true"
51
- class="shrink-0 align-middle"
52
- >
53
- <path d="M16 28V14" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
54
- <path d="M16 14C16 14 16 8 22 6C22 6 22 12 16 14Z" fill="currentColor" fill-opacity="0.85"></path>
55
- <path d="M16 14C16 14 16 8 10 6C10 6 10 12 16 14Z" fill="currentColor" fill-opacity="0.6"></path>
56
- <path d="M16 18C16 18 16 22 20 23" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
57
- <path d="M16 21C16 21 16 24 12 25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path>
58
- </svg>
59
- <span class="font-semibold">Grove</span>
60
- </a>
@@ -1,321 +0,0 @@
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 the record has a repository the root becomes an
14
- * `<article>` with a stretched title link, because nesting `<a>`
15
- * inside `<a>` is invalid HTML. The `owner/repo` line is a real
16
- * link to the repository and sits above the stretched overlay
17
- * (`relative z-10`).
18
- * - App icon top-left (48px), app name + clickable owner/repo,
19
- * two-line description with reserved vertical space.
20
- * - Footer row: stack logos on the left, stars and last-updated on
21
- * the right, navigation arrow anchored bottom-right.
22
- *
23
- * **The layout is fixed.** Earlier versions let list pages opt into a
24
- * curated badge, a license tag, a `category · status` line and
25
- * labelled chip rows, so the same record rendered differently
26
- * depending on where a visitor found it. Those flags are now
27
- * deprecated no-ops, as is `secondaryLinks` — the repository is
28
- * reached through the `owner/repo` line rather than a separate footer
29
- * button. Platform support is likewise off the card: it is reference
30
- * detail and belongs on the record page (`RecordSidebar`).
31
- *
32
- * Data can come from a full `record` or from explicit props (adapters
33
- * for shapes like `CollectionEntry` pass fields directly); explicit
34
- * props win over record-derived values.
35
- *
36
- * Uses the package's design-system tokens plus the standard `dark:`
37
- * variant. Equal-height cards in the row are achieved at the grid
38
- * level (`CardGrid`: `grid` + `items-stretch`).
39
- */
40
- import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
41
- import {
42
- getOwnerAndRepoFromRepoUrl,
43
- getOwnerAvatarUrl,
44
- formatRelative,
45
- formatStars,
46
- nameInitials,
47
- projectStackIds,
48
- truncateWords,
49
- } from "@grove-dev/astro";
50
- import CardIcon from "./CardIcon.astro";
51
- import Icon from "./Icon.astro";
52
-
53
- type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
54
-
55
- interface SecondaryLink {
56
- label: string;
57
- href: string;
58
- /** Adds target="_blank" + rel and the external-link glyph. */
59
- external?: boolean;
60
- /** Analytics hooks forwarded as data-event attributes. */
61
- event?: string;
62
- eventSource?: string;
63
- }
64
-
65
- interface Props {
66
- /** Record to render. Optional when explicit props are passed. */
67
- record?: CardRecord;
68
- /** Full detail-page URL. */
69
- href: string;
70
- /** Optional pre-resolved logo URL. Wins over the GitHub avatar. */
71
- logoUrl?: string;
72
- /** Override the description (defaults to `record.description`). */
73
- description?: string;
74
- /** Explicit overrides for record-derived values (adapter use). */
75
- name?: string;
76
- slug?: string;
77
- repoUrl?: string;
78
- stars?: number;
79
- pushedAt?: string | null;
80
- license?: string | null;
81
- category?: string;
82
- status?: string;
83
- curated?: boolean;
84
- stack?: string;
85
- /**
86
- * @deprecated Cards render the stack, not platform support — that
87
- * belongs on the record page. Accepted and ignored.
88
- */
89
- platforms?: string[];
90
- /**
91
- * @deprecated The card renders one fixed metadata set on every
92
- * surface. These flags used to add a curated badge, a license tag,
93
- * a `category · status` line, and labelled chip rows on list pages
94
- * only, which made the same record look like a different record
95
- * depending on where you found it. Accepted and ignored.
96
- */
97
- showCurated?: boolean;
98
- /** @deprecated Ignored — see `showCurated`. */
99
- showLicense?: boolean;
100
- /** @deprecated Ignored — see `showCurated`. */
101
- showCategoryStatus?: boolean;
102
- /** @deprecated Ignored — see `showCurated`. */
103
- labelledChips?: boolean;
104
- /** Footer links; presence switches the root to `<article>` mode. */
105
- /**
106
- * @deprecated Ignored. The repository is reached by clicking the
107
- * `owner/repo` line under the title, so cards no longer carry a
108
- * separate footer link button.
109
- */
110
- secondaryLinks?: SecondaryLink[];
111
- /** Eager-load the avatar (above-the-fold rows). */
112
- eager?: boolean;
113
- /** Analytics data-event attributes on the card root. */
114
- event?: string;
115
- eventSource?: string;
116
- class?: string;
117
- }
118
-
119
- const {
120
- record,
121
- href,
122
- logoUrl: logoUrlProp,
123
- description: descriptionProp,
124
- name: nameProp,
125
- slug: slugProp,
126
- repoUrl: repoUrlProp,
127
- stars: starsProp,
128
- pushedAt: pushedAtProp,
129
- stack: stackProp,
130
- eager = false,
131
- event,
132
- eventSource,
133
- class: className = "",
134
- } = Astro.props;
135
-
136
- const isProject = record?.kind === "project";
137
- const name =
138
- nameProp ??
139
- (record
140
- ? record.kind === "resource"
141
- ? (record as { title: string }).title
142
- : (record as { name: string }).name
143
- : "");
144
- const slug = slugProp ?? (record as { slug?: string } | undefined)?.slug;
145
- const repoHref =
146
- repoUrlProp ??
147
- (isProject
148
- ? (record as { repoUrl?: string }).repoUrl
149
- : (record as { links?: { github?: string } } | undefined)?.links?.github);
150
-
151
- const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
152
- const avatarUrl =
153
- logoUrlProp ??
154
- (isProject ? (record as { logoUrl?: string }).logoUrl : undefined) ??
155
- getOwnerAvatarUrl(owner, 80);
156
-
157
- const initials = nameInitials(name);
158
-
159
- // Trimmed on a word boundary before the two-line clamp gets to it: a
160
- // CSS clamp cuts at whatever character the box runs out of room on,
161
- // which is what produced "…and multi-", "…and retrieval", "…an" across
162
- // a grid.
163
- const description = truncateWords(
164
- descriptionProp ?? (record as { description?: string } | undefined)?.description ?? "",
165
- );
166
-
167
- const stars =
168
- starsProp ?? (isProject ? (record as { github?: { stars?: number } }).github?.stars : undefined);
169
- const pushedAt =
170
- pushedAtProp ??
171
- (isProject
172
- ? (record as { github?: { pushedAt?: string | null } }).github?.pushedAt
173
- : (record as { publishedAt?: string | null } | undefined)?.publishedAt);
174
- const stackIds = stackProp
175
- ? [stackProp]
176
- : isProject
177
- ? projectStackIds(record as { stack?: string; stacks?: string[] })
178
- : [];
179
-
180
- const starsLabel = formatStars(stars);
181
- const updatedLabel = formatRelative(pushedAt);
182
-
183
- const hasStars = starsLabel !== null && typeof stars === "number" && stars > 0;
184
- const hasUpdated = Boolean(pushedAt);
185
-
186
- // `<a>`-in-`<a>` is invalid HTML, and the `owner/repo` line is its own
187
- // link to the repository — so any card that shows one renders as an
188
- // `<article>` whose title link is stretched over the whole card.
189
- const isArticle = Boolean(repoHref && owner && repo);
190
- const Root = isArticle ? "article" : "a";
191
- const rootAttrs = isArticle ? {} : { href, "aria-label": `Open ${name}` };
192
-
193
- // The card's brand strip shows the record's STACK, not its platforms.
194
- // Stack is what distinguishes one card from its neighbours in a list;
195
- // platform support is reference detail, and it belongs on the record
196
- // page (`RecordSidebar`) where there is room to label it. Marks come
197
- // from the shared `Icon` component, which falls back to initials when
198
- // an asset is missing.
199
- const visibleStacks = stackIds.slice(0, 4);
200
- const stackOverflow = stackIds.length - visibleStacks.length;
201
- const hasStacks = visibleStacks.length > 0;
202
- ---
203
-
204
- <Root
205
- {...rootAttrs}
206
- class={`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-400 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-600 dark:hover:bg-ink-900 dark:focus-visible:border-ink-500 dark:focus-visible:ring-ink-100/15 ${className}`}
207
- data-record-slug={slug}
208
- data-event={event}
209
- data-event-source={eventSource}
210
- data-event-item={event ? slug : undefined}
211
- >
212
- {/* Header: icon + name + owner.
213
-
214
- The initials chip is always in the DOM, `hidden` behind the image,
215
- and unhidden by the image's own `onerror` — the same pattern
216
- `Icon.astro` uses. Rendering it only when `avatarUrl` is falsy made
217
- it unreachable for every record with a GitHub owner, so a dead
218
- logo URL or a renamed org showed the browser's broken-image glyph
219
- instead of a fallback. */}
220
- <header class="flex items-start gap-3">
221
- {avatarUrl && (
222
- <img
223
- src={avatarUrl}
224
- alt=""
225
- width="48"
226
- height="48"
227
- loading={eager ? "eager" : "lazy"}
228
- fetchpriority={eager ? "high" : undefined}
229
- decoding="async"
230
- onerror="this.hidden=true;this.nextElementSibling.hidden=false"
231
- class="h-12 w-12 shrink-0 rounded-lg bg-white object-contain p-1"
232
- />
233
- )}
234
- <span
235
- aria-hidden="true"
236
- hidden={Boolean(avatarUrl)}
237
- data-grove-card-fallback-initials
238
- 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"
239
- >
240
- {initials}
241
- </span>
242
-
243
- <div class="flex min-w-0 flex-1 flex-col gap-0.5">
244
- <h3 class="m-0 truncate text-[17px] font-semibold leading-snug tracking-tight text-ink-900 sm:text-[18px] dark:text-ink-100">
245
- {isArticle ? (
246
- <a
247
- href={href}
248
- class="text-inherit no-underline before:absolute before:inset-0 before:z-0 before:content-['']"
249
- >
250
- {name}
251
- </a>
252
- ) : (
253
- name
254
- )}
255
- </h3>
256
- {owner && repo && (
257
- <p class="m-0 truncate font-mono text-[13px] text-ink-500 dark:text-ink-400">
258
- {repoHref ? (
259
- <a
260
- href={repoHref}
261
- target="_blank"
262
- rel="noopener noreferrer"
263
- title={`${owner}/${repo} on GitHub`}
264
- data-event="external_repo_click"
265
- data-event-source="project_card_repo"
266
- data-event-item={slug}
267
- class="relative z-10 text-inherit no-underline transition-colors hover:text-ink-900 hover:underline dark:hover:text-ink-100"
268
- >
269
- {owner}<span class="opacity-60">/</span>{repo}
270
- </a>
271
- ) : (
272
- <Fragment>{owner}<span class="opacity-60">/</span>{repo}</Fragment>
273
- )}
274
- </p>
275
- )}
276
- </div>
277
- </header>
278
-
279
- {/* Description — two-line clamp with reserved vertical space */}
280
- <p
281
- class="m-0 line-clamp-2 min-h-[42px] text-[14px] leading-[1.5] text-ink-500 dark:text-ink-400"
282
- >
283
- {description}
284
- </p>
285
-
286
- {/* Footer: stack logos · stars/updated · arrow */}
287
- <footer class="mt-auto flex items-center justify-between gap-3 text-[12px] sm:text-[13px]">
288
- {hasStacks ? (
289
- <div class="flex min-w-0 items-center gap-2 text-ink-500 dark:text-ink-400">
290
- {visibleStacks.map((stack) => (
291
- <Icon name={stack} category="stack" size={16} title={stack} />
292
- ))}
293
- {stackOverflow > 0 && (
294
- <span class="text-[12px] text-ink-500 dark:text-ink-400">+{stackOverflow}</span>
295
- )}
296
- </div>
297
- ) : (
298
- /* Keeps the footer's stars/updated block right-aligned without
299
- shipping an empty flex container on every stackless card. */
300
- <span></span>
301
- )}
302
-
303
- <div class="flex min-w-0 shrink-0 items-center gap-3 text-ink-500 dark:text-ink-400">
304
- {hasStars && (
305
- <span class="inline-flex items-center gap-1" title={`${stars ?? 0} stars on GitHub`}>
306
- <span class="inline-flex text-amber-500"><CardIcon name="star" /></span>
307
- <span class="font-mono">{starsLabel}</span>
308
- </span>
309
- )}
310
- {hasUpdated && (
311
- <span class="whitespace-nowrap" title={`Last updated ${pushedAt}`}>
312
- Updated {updatedLabel}
313
- </span>
314
- )}
315
- <CardIcon
316
- name="arrow"
317
- class="shrink-0 text-ink-400 transition-colors group-hover:text-ink-700 dark:text-ink-500 dark:group-hover:text-ink-100"
318
- />
319
- </div>
320
- </footer>
321
- </Root>