@grove-dev/astro 0.4.0 → 0.5.0-next.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/package.json +3 -2
- package/src/components/CollectionIndex.astro +51 -16
- package/src/components/CollectionPage.astro +51 -35
- package/src/components/CollectionRow.astro +209 -72
- package/src/components/CollectionTeaser.astro +21 -59
- package/src/components/EditorialSummary.astro +57 -0
- package/src/components/IndexRow.astro +5 -5
- package/src/components/ItemCard.astro +11 -28
- package/src/components/LanguageBreakdown.astro +63 -0
- package/src/components/MarkdownBody.astro +37 -0
- package/src/components/RecordHeader.astro +156 -0
- package/src/components/RecordSidebar.astro +329 -0
- package/src/components/StackPlatformChips.astro +73 -0
- package/src/components/TableOfContents.astro +115 -0
- package/src/server/collections.test.ts +22 -0
- package/src/server/collections.ts +7 -0
- package/src/server/directory.ts +311 -65
- package/src/server/models.ts +155 -3
- package/src/styles.css +485 -59
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* CollectionTeaser — homepage-friendly subset of `CollectionIndex`.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper that defaults `limit` to 3 and sets homepage-friendly
|
|
6
|
+
* "View all collections" link + copy. For custom card counts or
|
|
7
|
+
* different copy, use `CollectionIndex` directly with explicit
|
|
8
|
+
* `limit` and `viewAllHref` props.
|
|
6
9
|
*/
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
10
|
+
import CollectionIndex from "./CollectionIndex.astro";
|
|
11
|
+
import type { CollectionIndexModel } from "../server/collections.js";
|
|
9
12
|
|
|
10
13
|
interface Props {
|
|
11
|
-
model:
|
|
14
|
+
model: CollectionIndexModel;
|
|
12
15
|
title?: string;
|
|
13
16
|
description?: string;
|
|
14
17
|
viewAllHref?: string;
|
|
15
18
|
viewAllLabel?: string;
|
|
19
|
+
/** Cap the number of cards rendered. Default: 3. */
|
|
20
|
+
limit?: number;
|
|
16
21
|
class?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -22,60 +27,17 @@ const {
|
|
|
22
27
|
description = "Hand-picked and auto-generated groupings of related items.",
|
|
23
28
|
viewAllHref = "/collections/",
|
|
24
29
|
viewAllLabel = "View all collections",
|
|
25
|
-
|
|
30
|
+
limit = 3,
|
|
31
|
+
class: className,
|
|
26
32
|
} = Astro.props;
|
|
27
33
|
---
|
|
28
34
|
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</h2>
|
|
39
|
-
<p class="m-0 mt-2 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
40
|
-
{description}
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
{viewAllHref && (
|
|
44
|
-
<a
|
|
45
|
-
href={viewAllHref}
|
|
46
|
-
class="hidden shrink-0 pb-2 text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
|
|
47
|
-
>
|
|
48
|
-
{viewAllLabel} →
|
|
49
|
-
</a>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
{model.collections.length === 0 ? (
|
|
54
|
-
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
55
|
-
No collections yet.
|
|
56
|
-
</p>
|
|
57
|
-
) : (
|
|
58
|
-
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
59
|
-
{model.collections.map((c) => (
|
|
60
|
-
<a
|
|
61
|
-
href={c.url}
|
|
62
|
-
class="flex flex-col gap-2 rounded-md border border-ink-200 bg-white p-5 transition-colors hover:border-ink-400 dark:border-ink-800 dark:bg-ink-950 dark:hover:border-ink-600"
|
|
63
|
-
>
|
|
64
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
65
|
-
{c.kind === "curated" ? "Curated" : "Generated"}
|
|
66
|
-
</span>
|
|
67
|
-
<span class="text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
68
|
-
{c.title}
|
|
69
|
-
</span>
|
|
70
|
-
<span class="text-[0.875rem] text-ink-500 dark:text-ink-400">
|
|
71
|
-
{c.description}
|
|
72
|
-
</span>
|
|
73
|
-
<span class="mt-auto pt-2 text-2xs font-medium text-ink-500 dark:text-ink-400">
|
|
74
|
-
{c.count} {c.count === 1 ? "item" : "items"} →
|
|
75
|
-
</span>
|
|
76
|
-
</a>
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
79
|
-
)}
|
|
80
|
-
</Container>
|
|
81
|
-
</section>
|
|
35
|
+
<CollectionIndex
|
|
36
|
+
model={model}
|
|
37
|
+
title={title}
|
|
38
|
+
description={description}
|
|
39
|
+
viewAllHref={viewAllHref}
|
|
40
|
+
viewAllLabel={viewAllLabel}
|
|
41
|
+
limit={limit}
|
|
42
|
+
class={className}
|
|
43
|
+
/>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EditorialSummary.astro
|
|
4
|
+
*
|
|
5
|
+
* Compact card at the top of the record body that surfaces the
|
|
6
|
+
* curator's "Best for" and "Consider before using" notes. Renders
|
|
7
|
+
* nothing when both arrays are empty.
|
|
8
|
+
*
|
|
9
|
+
* This is the editorial half of the page: the sidebar (right) is
|
|
10
|
+
* auto-generated facts; the summary (left) is human-authored
|
|
11
|
+
* curation.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
bestFor: string[];
|
|
15
|
+
caveats: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { bestFor, caveats } = Astro.props;
|
|
19
|
+
const hasAny = bestFor.length > 0 || caveats.length > 0;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{hasAny && (
|
|
23
|
+
<section class="grove-editorial mb-10 rounded-[var(--radius-lg)] border border-ink-200 bg-background p-5 dark:border-ink-800" aria-label="Editorial summary">
|
|
24
|
+
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
25
|
+
{bestFor.length > 0 && (
|
|
26
|
+
<div>
|
|
27
|
+
<h2 class="m-0 flex items-center gap-1.5 text-2xs font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-400">
|
|
28
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
29
|
+
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
|
30
|
+
</svg>
|
|
31
|
+
Best for
|
|
32
|
+
</h2>
|
|
33
|
+
<ul class="mt-2 m-0 flex list-none flex-col gap-1.5 p-0 text-sm text-ink-700 dark:text-ink-200">
|
|
34
|
+
{bestFor.map((b) => (
|
|
35
|
+
<li class="leading-relaxed">{b}</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
{caveats.length > 0 && (
|
|
41
|
+
<div>
|
|
42
|
+
<h2 class="m-0 flex items-center gap-1.5 text-2xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
|
43
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
44
|
+
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm9 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 .4 1.04l1.6.8a.75.75 0 1 0 .67-1.34l-1.17-.59V4.75z" />
|
|
45
|
+
</svg>
|
|
46
|
+
Consider before using
|
|
47
|
+
</h2>
|
|
48
|
+
<ul class="mt-2 m-0 flex list-none flex-col gap-1.5 p-0 text-sm text-ink-700 dark:text-ink-200">
|
|
49
|
+
{caveats.map((c) => (
|
|
50
|
+
<li class="leading-relaxed">{c}</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { IndexRecord } from "@grove-dev/core";
|
|
3
|
-
import
|
|
3
|
+
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
4
4
|
import { formatRelative, formatStars, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds, statusDisplay } from "@grove-dev/astro";
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
@@ -75,10 +75,10 @@ const curated = Boolean(item.curation?.reviewed || (project && (project.bestFor.
|
|
|
75
75
|
</dl>
|
|
76
76
|
|
|
77
77
|
{showChips && (platforms.length > 0 || stacks.length > 0) && (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
<StackPlatformChips
|
|
79
|
+
stack={stacks[0]}
|
|
80
|
+
platform={platforms}
|
|
81
|
+
/>
|
|
82
82
|
)}
|
|
83
83
|
|
|
84
84
|
<footer class="relative z-10 mt-1 flex items-center gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* pattern is to pass `routeSlug` (or `href` directly).
|
|
25
25
|
*/
|
|
26
26
|
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
27
|
-
import
|
|
27
|
+
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
28
28
|
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds } from "@grove-dev/astro";
|
|
29
29
|
import { formatStars, formatRelative } from "@grove-dev/astro";
|
|
30
30
|
import { statusDisplay } from "@grove-dev/astro";
|
|
@@ -279,34 +279,17 @@ const category = (item as { category?: string }).category;
|
|
|
279
279
|
</div>
|
|
280
280
|
)}
|
|
281
281
|
|
|
282
|
-
{/* Platform + stack chips, pushed to the bottom.
|
|
282
|
+
{/* Platform + stack chips, pushed to the bottom. Shared with
|
|
283
|
+
`IndexRow` / `CollectionRow` so the labelled-row pattern is
|
|
284
|
+
consistent across all card-shaped components. */}
|
|
283
285
|
{showChips && (
|
|
284
|
-
<div class="mt-auto
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
</span>
|
|
292
|
-
))}
|
|
293
|
-
{platformOverflow > 0 && (
|
|
294
|
-
<span class="inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground text-ink-400">+{platformOverflow}</span>
|
|
295
|
-
)}
|
|
296
|
-
</div>
|
|
297
|
-
)}
|
|
298
|
-
{visibleStacks.length > 0 && (
|
|
299
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
300
|
-
{visibleStacks.map((s) => (
|
|
301
|
-
<span class="inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground !bg-muted !text-muted-foreground">
|
|
302
|
-
<Icon name={s} category="stack" size={11} />
|
|
303
|
-
<span>{s}</span>
|
|
304
|
-
</span>
|
|
305
|
-
))}
|
|
306
|
-
{stackOverflow > 0 && (
|
|
307
|
-
<span class="inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground !bg-muted !text-muted-foreground text-ink-400">+{stackOverflow}</span>
|
|
308
|
-
)}
|
|
309
|
-
</div>
|
|
286
|
+
<div class="mt-auto">
|
|
287
|
+
<StackPlatformChips
|
|
288
|
+
stack={visibleStacks[0]}
|
|
289
|
+
platform={visiblePlatforms}
|
|
290
|
+
/>
|
|
291
|
+
{stackOverflow > 0 && (
|
|
292
|
+
<span class="text-2xs text-ink-400 dark:text-ink-600">+{stackOverflow} more stacks</span>
|
|
310
293
|
)}
|
|
311
294
|
</div>
|
|
312
295
|
)}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LanguageBreakdown.astro
|
|
4
|
+
*
|
|
5
|
+
* Visual GitHub-Linguist-style code-composition bar plus a legend
|
|
6
|
+
* showing each language's percentage. Reads from the record's
|
|
7
|
+
* pre-computed `languages` array (top-5 by bytes) and `langColor`
|
|
8
|
+
* (in @grove-dev/core).
|
|
9
|
+
*
|
|
10
|
+
* Used inside the Activity sidebar card.
|
|
11
|
+
*/
|
|
12
|
+
import { langColor } from "@grove-dev/core";
|
|
13
|
+
|
|
14
|
+
interface LangEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
bytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Top-N languages as `[name, bytes]` tuples (already sorted). */
|
|
21
|
+
languages: ReadonlyArray<readonly [string, number]>;
|
|
22
|
+
/** Total bytes across all languages (denominator). */
|
|
23
|
+
totalBytes: number;
|
|
24
|
+
/** Optional max segments to show. */
|
|
25
|
+
maxSegments?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { languages, totalBytes, maxSegments = 5 } = Astro.props;
|
|
29
|
+
|
|
30
|
+
const segments = languages.slice(0, maxSegments).map(([name, bytes]) => {
|
|
31
|
+
const pct = Math.round((bytes / Math.max(1, totalBytes)) * 100);
|
|
32
|
+
return { name, pct, color: langColor(name) };
|
|
33
|
+
});
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
{segments.length > 0 && (
|
|
37
|
+
<div class="grove-langbreak mt-4 border-t border-ink-100 pt-4 dark:border-ink-800">
|
|
38
|
+
<h3 class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
39
|
+
Code composition
|
|
40
|
+
</h3>
|
|
41
|
+
<div class="grove-langbreak-bar mt-2 flex h-2 w-full overflow-hidden rounded-full bg-ink-100 dark:bg-ink-800">
|
|
42
|
+
{segments.map((s) => (
|
|
43
|
+
<span
|
|
44
|
+
class="grove-langbreak-seg h-full"
|
|
45
|
+
style={`width:${s.pct}%;background-color:${s.color}`}
|
|
46
|
+
title={`${s.name}: ${s.pct}%`}
|
|
47
|
+
/>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
<ul class="grove-langbreak-list mt-2 flex flex-wrap gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
51
|
+
{segments.map((s) => (
|
|
52
|
+
<li class="inline-flex items-center gap-1">
|
|
53
|
+
<span
|
|
54
|
+
class="inline-block h-2 w-2 rounded-sm"
|
|
55
|
+
style={`background-color:${s.color}`}
|
|
56
|
+
/>
|
|
57
|
+
<span class="font-mono">{s.name}</span>
|
|
58
|
+
<span>{s.pct}%</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* MarkdownBody.astro
|
|
4
|
+
*
|
|
5
|
+
* Renders a record's pre-sanitized Markdown body (`getContentHtml`)
|
|
6
|
+
* inside the package's `.grove-prose` stylesheet.
|
|
7
|
+
*
|
|
8
|
+
* The renderer itself lives in `@grove-dev/astro/server/directory.ts`
|
|
9
|
+
* (`renderMarkdownToSafeHtml`) and runs once per record at module
|
|
10
|
+
* load; this component is the presentation half of that pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Props:
|
|
13
|
+
* - `recordSlug`: the **record's** slug (not the route prefix).
|
|
14
|
+
* `getContentHtml` is keyed by record slug — `detail.slug` is the
|
|
15
|
+
* route prefix (e.g. "projects"), `detail.record.slug` is the
|
|
16
|
+
* record (e.g. "crewai").
|
|
17
|
+
* - `fallback`: optional content to render when the record has no
|
|
18
|
+
* body (e.g. an empty-state message).
|
|
19
|
+
*/
|
|
20
|
+
import { getContentHtml } from "../server/directory.js";
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Record slug — looks up the pre-rendered HTML in the module map. */
|
|
24
|
+
recordSlug: string;
|
|
25
|
+
/** Optional fallback content for records without a body. */
|
|
26
|
+
fallback?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { recordSlug, fallback } = Astro.props;
|
|
30
|
+
const html = getContentHtml(recordSlug);
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
{html ? (
|
|
34
|
+
<div class="grove-prose" set:html={html} />
|
|
35
|
+
) : fallback ? (
|
|
36
|
+
<p class="grove-prose text-ink-500 dark:text-ink-400">{fallback}</p>
|
|
37
|
+
) : null}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* RecordHeader.astro
|
|
4
|
+
*
|
|
5
|
+
* Compact project-identity header used at the top of every record
|
|
6
|
+
* detail page. The header carries:
|
|
7
|
+
* - avatar / initials
|
|
8
|
+
* - pills row (category + curation labels + "Curated" check +
|
|
9
|
+
* optional activity badge)
|
|
10
|
+
* - record name + owner/repo line
|
|
11
|
+
* - short description
|
|
12
|
+
* - GitHub + Homepage CTAs
|
|
13
|
+
*
|
|
14
|
+
* Tags + activity metadata are surfaced separately (sidebar), so
|
|
15
|
+
* the header stays narrow on mobile and doesn't compete with the
|
|
16
|
+
* two-column layout below.
|
|
17
|
+
*
|
|
18
|
+
* Tone classes for the activity badge are static `[data-activity-tone]`
|
|
19
|
+
* selectors in `styles.css` — Tailwind purges any classes built from
|
|
20
|
+
* dynamic strings, so the package ships the rules statically and
|
|
21
|
+
* the badge's `data-activity-tone` attribute picks one at runtime.
|
|
22
|
+
*/
|
|
23
|
+
import type { RecordDetailModel } from "../server/models.js";
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
detail: RecordDetailModel;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { detail } = Astro.props;
|
|
30
|
+
const {
|
|
31
|
+
record,
|
|
32
|
+
repoUrl,
|
|
33
|
+
homepageUrl,
|
|
34
|
+
ownerRepo,
|
|
35
|
+
avatarSrc,
|
|
36
|
+
initials,
|
|
37
|
+
curationLabels,
|
|
38
|
+
activityBadge,
|
|
39
|
+
category,
|
|
40
|
+
description,
|
|
41
|
+
name,
|
|
42
|
+
} = detail;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<header class="mb-10 flex flex-col gap-5 sm:flex-row sm:items-start sm:gap-6">
|
|
46
|
+
{avatarSrc ? (
|
|
47
|
+
<img
|
|
48
|
+
src={avatarSrc}
|
|
49
|
+
alt=""
|
|
50
|
+
width="64"
|
|
51
|
+
height="64"
|
|
52
|
+
loading="lazy"
|
|
53
|
+
decoding="async"
|
|
54
|
+
class="h-14 w-14 shrink-0 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950 sm:h-16 sm:w-16"
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<span
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
class="inline-flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200 sm:h-16 sm:w-16"
|
|
60
|
+
>
|
|
61
|
+
{initials || "·"}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<div class="min-w-0 flex-1">
|
|
66
|
+
{/* Pills row — category + curation + Curated check + activity badge. */}
|
|
67
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
68
|
+
{category && (
|
|
69
|
+
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
70
|
+
{category}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
{curationLabels.map((l) => (
|
|
74
|
+
<span class="rounded-full bg-secondary px-2 py-0.5 text-2xs font-medium text-secondary-foreground">
|
|
75
|
+
{l}
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
{record.curation?.reviewed && (
|
|
79
|
+
<span class="inline-flex items-center gap-1 rounded border border-emerald-300/60 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300">
|
|
80
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
81
|
+
<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" />
|
|
82
|
+
</svg>
|
|
83
|
+
Curated
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
{activityBadge && (
|
|
87
|
+
<span
|
|
88
|
+
data-activity-tone={activityBadge.tone}
|
|
89
|
+
class="grove-pill grove-pill--activity inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-medium"
|
|
90
|
+
>
|
|
91
|
+
<span class="relative flex h-1.5 w-1.5">
|
|
92
|
+
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-60"></span>
|
|
93
|
+
<span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-current"></span>
|
|
94
|
+
</span>
|
|
95
|
+
{activityBadge.label}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<h1 class="m-0 mt-2 text-[2rem] font-semibold leading-tight tracking-tight text-ink-900 dark:text-ink-100 sm:text-[2.25rem]">
|
|
101
|
+
{name}
|
|
102
|
+
</h1>
|
|
103
|
+
|
|
104
|
+
{ownerRepo && (
|
|
105
|
+
<p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
|
|
106
|
+
<a
|
|
107
|
+
href={`https://github.com/${ownerRepo.owner}`}
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel="noopener noreferrer"
|
|
110
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
111
|
+
>
|
|
112
|
+
@{ownerRepo.owner}
|
|
113
|
+
</a>
|
|
114
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
115
|
+
<a
|
|
116
|
+
href={repoUrl}
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noopener noreferrer"
|
|
119
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
120
|
+
>
|
|
121
|
+
{ownerRepo.repo}
|
|
122
|
+
</a>
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{description && (
|
|
127
|
+
<p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
128
|
+
{description}
|
|
129
|
+
</p>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* CTA row — primary actions only. */}
|
|
133
|
+
<div class="mt-4 flex flex-wrap items-center gap-2">
|
|
134
|
+
{repoUrl && (
|
|
135
|
+
<a
|
|
136
|
+
href={repoUrl}
|
|
137
|
+
target="_blank"
|
|
138
|
+
rel="noopener noreferrer"
|
|
139
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90"
|
|
140
|
+
>
|
|
141
|
+
View on GitHub →
|
|
142
|
+
</a>
|
|
143
|
+
)}
|
|
144
|
+
{homepageUrl && (
|
|
145
|
+
<a
|
|
146
|
+
href={homepageUrl}
|
|
147
|
+
target="_blank"
|
|
148
|
+
rel="noopener noreferrer"
|
|
149
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-border bg-secondary px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 text-foreground hover:bg-accent"
|
|
150
|
+
>
|
|
151
|
+
Homepage
|
|
152
|
+
</a>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</header>
|