@grove-dev/astro 0.6.1 → 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.
- package/README.md +77 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.js +1 -1
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/icon-kinds.d.ts +1 -1
- package/dist/lib/icon-kinds.js +43 -43
- package/dist/lib/icon-registry.d.ts +2 -2
- package/dist/lib/icon-registry.js +23 -23
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -9
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -1
- package/dist/lib/lenses.js +1 -1
- package/dist/lib/packaged-icons.d.ts +1 -1
- package/dist/lib/packaged-icons.js +4 -4
- package/dist/lib/repo.d.ts +1 -1
- package/dist/lib/repo.js +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -1
- package/dist/lib/taxonomy-counts.js +1 -1
- package/package.json +7 -17
- package/src/index.ts +23 -38
- package/src/lib/display.ts +1 -1
- package/src/lib/format.ts +1 -1
- package/src/lib/icon-kinds.ts +44 -44
- package/src/lib/icon-registry.ts +25 -25
- package/src/lib/index.ts +9 -9
- package/src/lib/lenses.ts +1 -1
- package/src/lib/packaged-icons.ts +8 -8
- package/src/lib/repo.ts +1 -1
- package/src/lib/search.ts +1 -1
- package/src/lib/taxonomy-counts.ts +1 -1
- package/src/server/collections.ts +53 -42
- package/src/server/contrast.ts +14 -9
- package/src/server/directory.ts +22 -17
- package/src/server/index.ts +3 -2
- package/src/server/models.ts +404 -213
- package/src/server/seo.ts +34 -31
- package/src/styles.css +235 -91
- package/dist/layouts/index.d.ts +0 -25
- package/dist/layouts/index.d.ts.map +0 -1
- package/dist/layouts/index.js +0 -25
- package/dist/layouts/index.js.map +0 -1
- package/dist/ui/button.d.ts +0 -53
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -90
- package/dist/ui/button.js.map +0 -1
- package/src/components/CardGrid.astro +0 -41
- package/src/components/CardIcon.astro +0 -67
- package/src/components/CategoryGrid.astro +0 -114
- package/src/components/CollectionCard.astro +0 -56
- package/src/components/CollectionIndex.astro +0 -102
- package/src/components/CollectionPage.astro +0 -128
- package/src/components/CollectionRow.astro +0 -81
- package/src/components/CollectionTeaser.astro +0 -47
- package/src/components/ContributorsGrid.astro +0 -143
- package/src/components/DirectoryIndexClient.astro +0 -510
- package/src/components/EditorialSummary.astro +0 -58
- package/src/components/FilterGroupMenu.astro +0 -69
- package/src/components/FilterOptions.astro +0 -49
- package/src/components/FinalCta.astro +0 -43
- package/src/components/Hero.astro +0 -243
- package/src/components/Icon.astro +0 -191
- package/src/components/IndexRow.astro +0 -37
- package/src/components/LanguageBreakdown.astro +0 -67
- package/src/components/MarkdownBody.astro +0 -37
- package/src/components/OriginalCollection.astro +0 -117
- package/src/components/Pagination.astro +0 -50
- package/src/components/PoweredBy.astro +0 -60
- package/src/components/ProjectCard.astro +0 -321
- package/src/components/RecordHeader.astro +0 -221
- package/src/components/RecordSection.astro +0 -128
- package/src/components/RecordSidebar.astro +0 -387
- package/src/components/RefinePanel.astro +0 -168
- package/src/components/SmartLensTabs.astro +0 -81
- package/src/components/StackGrid.astro +0 -151
- package/src/components/StackPlatformChips.astro +0 -73
- package/src/components/SubmissionClient.astro +0 -254
- package/src/components/TableOfContents.astro +0 -140
- package/src/components/WhyThisExists.astro +0 -116
- package/src/layouts/BaseLayout.astro +0 -415
- package/src/layouts/Container.astro +0 -18
- package/src/layouts/Footer.astro +0 -231
- package/src/layouts/Header.astro +0 -214
- package/src/layouts/SectionHeader.astro +0 -43
- package/src/layouts/Seo.astro +0 -171
- package/src/layouts/ThemeToggle.astro +0 -118
- package/src/layouts/astro-shim.d.ts +0 -29
- package/src/layouts/index.ts +0 -24
- package/src/server/collections.test.ts +0 -212
- package/src/server/contrast.test.ts +0 -82
- package/src/server/directory.test.ts +0 -177
- package/src/server/github-repo.test.ts +0 -88
- package/src/server/models-home.test.ts +0 -101
- package/src/server/models.test.ts +0 -135
- package/src/server/seo.test.ts +0 -123
- package/src/ui/Badge.astro +0 -38
- package/src/ui/Button.astro +0 -42
- package/src/ui/EmptyState.astro +0 -38
- package/src/ui/FilterDrawer.astro +0 -124
- package/src/ui/PageHeader.astro +0 -39
- package/src/ui/SearchField.astro +0 -106
- package/src/ui/button.test.ts +0 -54
- 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://grove.dev.mn",
|
|
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>
|