@grove-dev/astro 0.5.0-next.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +0 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/ui/button.d.ts +45 -0
- package/dist/ui/button.d.ts.map +1 -0
- package/dist/ui/button.js +82 -0
- package/dist/ui/button.js.map +1 -0
- package/package.json +9 -5
- package/src/components/CardGrid.astro +41 -0
- package/src/components/CardIcon.astro +67 -0
- package/src/components/CategoryGrid.astro +20 -5
- package/src/components/CollectionCard.astro +56 -0
- package/src/components/CollectionIndex.astro +21 -26
- package/src/components/CollectionPage.astro +36 -13
- package/src/components/CollectionRow.astro +62 -192
- package/src/components/CollectionTeaser.astro +4 -0
- package/src/components/ContributorsGrid.astro +4 -1
- package/src/components/DirectoryIndexClient.astro +127 -30
- package/src/components/EditorialSummary.astro +6 -5
- package/src/components/FilterGroupMenu.astro +24 -10
- package/src/components/FilterOptions.astro +6 -1
- package/src/components/FinalCta.astro +5 -3
- package/src/components/Hero.astro +85 -139
- package/src/components/IndexRow.astro +36 -69
- package/src/components/LanguageBreakdown.astro +5 -1
- package/src/components/OriginalCollection.astro +3 -2
- package/src/components/Pagination.astro +7 -20
- package/src/components/ProjectCard.astro +345 -0
- package/src/components/RecordHeader.astro +111 -82
- package/src/components/RecordSection.astro +14 -10
- package/src/components/RecordSidebar.astro +68 -20
- package/src/components/RefinePanel.astro +41 -62
- package/src/components/SmartLensTabs.astro +3 -7
- package/src/components/StackGrid.astro +26 -6
- package/src/components/SubmissionClient.astro +140 -63
- package/src/components/TableOfContents.astro +31 -5
- package/src/components/WhyThisExists.astro +1 -1
- package/src/index.ts +2 -2
- package/src/layouts/BaseLayout.astro +79 -9
- package/src/layouts/Header.astro +5 -4
- package/src/layouts/SectionHeader.astro +3 -2
- package/src/layouts/Seo.astro +28 -17
- package/src/layouts/ThemeToggle.astro +23 -6
- package/src/lib/index.ts +0 -2
- package/src/server/collections.ts +11 -0
- package/src/server/contrast.test.ts +82 -0
- package/src/server/contrast.ts +117 -0
- package/src/server/directory.test.ts +177 -0
- package/src/server/directory.ts +328 -206
- package/src/server/github-repo.test.ts +88 -0
- package/src/server/github-repo.ts +104 -0
- package/src/server/index.ts +4 -3
- package/src/server/models-home.test.ts +101 -0
- package/src/server/models.test.ts +135 -0
- package/src/server/models.ts +170 -42
- package/src/styles.css +128 -16
- package/src/ui/Badge.astro +38 -0
- package/src/ui/Button.astro +42 -0
- package/src/ui/EmptyState.astro +38 -0
- package/src/ui/FilterDrawer.astro +104 -0
- package/src/ui/PageHeader.astro +39 -0
- package/src/ui/SearchField.astro +50 -0
- package/src/ui/button.test.ts +54 -0
- package/src/ui/button.ts +98 -0
- package/dist/lib/scores.d.ts +0 -2
- package/dist/lib/scores.d.ts.map +0 -1
- package/dist/lib/scores.js +0 -2
- package/dist/lib/scores.js.map +0 -1
- package/src/components/CurationGrid.astro +0 -41
- package/src/components/DecisionRow.astro +0 -89
- package/src/components/ExploreByCategory.astro +0 -67
- package/src/components/ExploreByStack.astro +0 -78
- package/src/components/GroveDocumentHead.astro +0 -28
- package/src/components/ItemCard.astro +0 -324
- package/src/components/MinimalAbout.astro +0 -82
- package/src/components/ScoreBars.astro +0 -102
- package/src/lib/scores.ts +0 -1
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* ItemCard — compact card for one record on the home page (or any
|
|
4
|
-
* 3-column grid).
|
|
5
|
-
*
|
|
6
|
-
* V1 (published) API: takes `record` (a Resource / IndexProjectRecord)
|
|
7
|
-
* and `href` (the full detail-page URL). The page that calls this
|
|
8
|
-
* component owns the path layout — blueprint-aware consumers
|
|
9
|
-
* (project-directory → /projects/<slug>, resource-hub →
|
|
10
|
-
* /resources/<slug>, ecosystem-map → /entities/<slug>) pass the
|
|
11
|
-
* correct href via `RecordSection`'s `hrefFor` callback.
|
|
12
|
-
*
|
|
13
|
-
* V1 (alternative) API: pass `routeSlug` (e.g. `"projects"`,
|
|
14
|
-
* `"resources"`, `"entities"`) to let the component derive the
|
|
15
|
-
* detail URL itself as `/${routeSlug}/${slug}`. This is a thin
|
|
16
|
-
* alternative to passing `href`; the page-level `hrefFor` callback
|
|
17
|
-
* is still the recommended API.
|
|
18
|
-
*
|
|
19
|
-
* V1 (legacy) API: also accepts `item` + `pathPrefix` as a
|
|
20
|
-
* convenience for callers that haven't migrated. When any of
|
|
21
|
-
* `href` / `routeSlug` / `pathPrefix` is provided, the priority is
|
|
22
|
-
* `href` > `routeSlug` > `pathPrefix`. The `pathPrefix` default
|
|
23
|
-
* is `"/items"` for V0 backwards-compat, but the canonical V1
|
|
24
|
-
* pattern is to pass `routeSlug` (or `href` directly).
|
|
25
|
-
*/
|
|
26
|
-
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
27
|
-
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
28
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds } from "@grove-dev/astro";
|
|
29
|
-
import { formatStars, formatRelative } from "@grove-dev/astro";
|
|
30
|
-
import { statusDisplay } from "@grove-dev/astro";
|
|
31
|
-
import { licenseDisplay } from "@grove-dev/astro";
|
|
32
|
-
|
|
33
|
-
type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
|
|
34
|
-
|
|
35
|
-
interface Props {
|
|
36
|
-
/** V1 API — full Resource/IndexProjectRecord. */
|
|
37
|
-
record?: CardRecord;
|
|
38
|
-
/** V1 API — full detail-page URL. Wins over `routeSlug` / `pathPrefix`. */
|
|
39
|
-
href?: string;
|
|
40
|
-
/** V1 API — blueprint route slug (e.g. "projects", "resources",
|
|
41
|
-
* "entities"). When `href` is not provided, the detail URL is
|
|
42
|
-
* derived as `/${routeSlug}/${slug}`. */
|
|
43
|
-
routeSlug?: string;
|
|
44
|
-
/** Legacy API — singular item, equivalent to `record`. */
|
|
45
|
-
item?: CardRecord;
|
|
46
|
-
/** Legacy API — URL prefix, equivalent to deriving from
|
|
47
|
-
* blueprint routeSlug + slug. Default `"/items"` (V0 leftover). */
|
|
48
|
-
pathPrefix?: string;
|
|
49
|
-
/** When true, show owner/repo below the title. Default true. */
|
|
50
|
-
showOwner?: boolean;
|
|
51
|
-
/** When false, hide the platform + stack chip row. Default true. */
|
|
52
|
-
showChips?: boolean;
|
|
53
|
-
/** When false, hide the curated label. Default true. */
|
|
54
|
-
showLabel?: boolean;
|
|
55
|
-
/** When false, hide the stars/updated/license metadata row. */
|
|
56
|
-
showMeta?: boolean;
|
|
57
|
-
class?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const {
|
|
61
|
-
record: recordProp,
|
|
62
|
-
href: hrefProp,
|
|
63
|
-
routeSlug,
|
|
64
|
-
item: itemProp,
|
|
65
|
-
pathPrefix = "/items",
|
|
66
|
-
showOwner = true,
|
|
67
|
-
showChips = true,
|
|
68
|
-
showLabel = true,
|
|
69
|
-
showMeta = true,
|
|
70
|
-
class: className = "",
|
|
71
|
-
} = Astro.props;
|
|
72
|
-
|
|
73
|
-
// Resolve `item` (legacy) → `record` (V1) and `hrefProp` → final.
|
|
74
|
-
const item: CardRecord = (recordProp ?? itemProp) as CardRecord;
|
|
75
|
-
const slug = (item as { slug: string }).slug;
|
|
76
|
-
let detailHref: string;
|
|
77
|
-
if (typeof hrefProp === "string" && hrefProp.length > 0) {
|
|
78
|
-
detailHref = hrefProp;
|
|
79
|
-
} else if (typeof routeSlug === "string" && routeSlug.length > 0) {
|
|
80
|
-
// V1 alternative API: blueprint routeSlug → /<routeSlug>/<slug>.
|
|
81
|
-
const base = routeSlug.replace(/^\/+|\/+$/g, "");
|
|
82
|
-
detailHref = base ? `/${base}/${slug}` : `/${slug}`;
|
|
83
|
-
} else {
|
|
84
|
-
// V0 legacy fallback: explicit `pathPrefix` (default "/items").
|
|
85
|
-
const base = pathPrefix.replace(/\/$/, "");
|
|
86
|
-
detailHref = `${base}/${slug}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const isProject = item.kind === "project";
|
|
90
|
-
const name = item.kind === "resource" ? (item as { title: string }).title : (item as { name: string }).name;
|
|
91
|
-
const repoHref = isProject
|
|
92
|
-
? (item as { repoUrl?: string }).repoUrl
|
|
93
|
-
: (item as { links: { github?: string } }).links?.github;
|
|
94
|
-
const homepageHref = isProject
|
|
95
|
-
? undefined
|
|
96
|
-
: (item.links as { website?: string; source?: string }).website ??
|
|
97
|
-
(item.links as { source?: string }).source;
|
|
98
|
-
|
|
99
|
-
// Curated = at least one of the hand-written fields is filled in.
|
|
100
|
-
const isCurated = Boolean(
|
|
101
|
-
(item as { curation?: { reviewed?: boolean } }).curation?.reviewed ||
|
|
102
|
-
(isProject && (
|
|
103
|
-
((item as { bestFor?: string[] }).bestFor?.length ?? 0) > 0 ||
|
|
104
|
-
((item as { whyListed?: string[] }).whyListed?.length ?? 0) > 0 ||
|
|
105
|
-
((item as { caveats?: string[] }).caveats?.length ?? 0) > 0
|
|
106
|
-
)),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Initials for placeholder logo.
|
|
110
|
-
const initials = (name ?? "")
|
|
111
|
-
.split(/\s+/)
|
|
112
|
-
.map((w) => w[0])
|
|
113
|
-
.filter(Boolean)
|
|
114
|
-
.slice(0, 2)
|
|
115
|
-
.join("")
|
|
116
|
-
.toUpperCase();
|
|
117
|
-
|
|
118
|
-
// Logo URL: explicit `logoUrl` (project only) → GitHub owner avatar
|
|
119
|
-
// → initials placeholder.
|
|
120
|
-
const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
|
|
121
|
-
const logoUrl = isProject
|
|
122
|
-
? (item as { logoUrl?: string }).logoUrl ?? getOwnerAvatarUrl(owner, 80)
|
|
123
|
-
: undefined;
|
|
124
|
-
|
|
125
|
-
// Up to 4 platforms + stack chips (project only).
|
|
126
|
-
const platforms = isProject ? (item as { platforms?: string[] }).platforms ?? [] : [];
|
|
127
|
-
const visiblePlatforms = platforms.slice(0, 4);
|
|
128
|
-
const platformOverflow = platforms.length - visiblePlatforms.length;
|
|
129
|
-
|
|
130
|
-
const allStacks = isProject ? projectStackIds(item as ProjectRecord) : [];
|
|
131
|
-
const visibleStacks = allStacks.slice(0, 3);
|
|
132
|
-
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
133
|
-
|
|
134
|
-
const stars = isProject ? (item as { github?: { stars?: number } }).github?.stars : undefined;
|
|
135
|
-
const updatedAt = isProject
|
|
136
|
-
? (item as { github?: { pushedAt?: string | null } }).github?.pushedAt
|
|
137
|
-
: (item as { publishedAt?: string | null }).publishedAt;
|
|
138
|
-
const starsLabel = formatStars(stars);
|
|
139
|
-
const updatedLabel = formatRelative(updatedAt);
|
|
140
|
-
const license = isProject ? (item as { github?: { license?: string } }).github?.license : null;
|
|
141
|
-
const status = isProject ? (item as { health?: { status?: string } }).health?.status : undefined;
|
|
142
|
-
|
|
143
|
-
const externalHref = repoHref ?? homepageHref;
|
|
144
|
-
const statusText = status ? statusDisplay(status as Parameters<typeof statusDisplay>[0]) : "";
|
|
145
|
-
const category = (item as { category?: string }).category;
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
<article
|
|
149
|
-
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}`}
|
|
150
|
-
>
|
|
151
|
-
{/* Top row: logo + name + (optional) status + curated mark. */}
|
|
152
|
-
<header class="flex items-start gap-3">
|
|
153
|
-
{logoUrl ? (
|
|
154
|
-
<img
|
|
155
|
-
src={logoUrl}
|
|
156
|
-
alt={`${name} avatar`}
|
|
157
|
-
width="40"
|
|
158
|
-
height="40"
|
|
159
|
-
loading="lazy"
|
|
160
|
-
decoding="async"
|
|
161
|
-
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"
|
|
162
|
-
/>
|
|
163
|
-
) : (
|
|
164
|
-
<span
|
|
165
|
-
aria-hidden="true"
|
|
166
|
-
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"
|
|
167
|
-
>
|
|
168
|
-
{initials}
|
|
169
|
-
</span>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
173
|
-
<h3
|
|
174
|
-
class="m-0 truncate text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100"
|
|
175
|
-
>
|
|
176
|
-
{/* Name → detail page (not GitHub repo). */}
|
|
177
|
-
<a
|
|
178
|
-
href={detailHref}
|
|
179
|
-
class="inline-flex min-h-6 min-w-6 items-center text-inherit no-underline"
|
|
180
|
-
>
|
|
181
|
-
{name}
|
|
182
|
-
</a>
|
|
183
|
-
</h3>
|
|
184
|
-
{showLabel && (
|
|
185
|
-
<span class="truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
186
|
-
{showOwner && owner ? (
|
|
187
|
-
<>
|
|
188
|
-
<a
|
|
189
|
-
href={repoHref}
|
|
190
|
-
target="_blank"
|
|
191
|
-
rel="noopener noreferrer"
|
|
192
|
-
data-event="external_repo_click"
|
|
193
|
-
data-event-source="card_owner"
|
|
194
|
-
data-event-item={slug}
|
|
195
|
-
class="font-mono text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
196
|
-
>
|
|
197
|
-
{/* `data-event` and `data-event-*` are forwarding hooks
|
|
198
|
-
for downstream analytics. Sites wire their own
|
|
199
|
-
gtag/PostHog/Plausible handler in BaseLayout.astro
|
|
200
|
-
to read these and forward to their analytics
|
|
201
|
-
provider. The V1 names are stable: `data-event` is
|
|
202
|
-
the action ("external_repo_click"), `data-event-source`
|
|
203
|
-
is the surface ("card_owner" | "card_view_repo" |
|
|
204
|
-
"detail_owner" | "detail_view_github" | "detail_homepage"
|
|
205
|
-
| "detail_distribution"), and `data-event-item` is
|
|
206
|
-
the record slug. */}
|
|
207
|
-
<span class="text-ink-400">@</span>{owner}{repo ? <span class="text-ink-400">/{repo}</span> : ""}
|
|
208
|
-
</a>
|
|
209
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
210
|
-
{category}
|
|
211
|
-
{status && (
|
|
212
|
-
<>
|
|
213
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
214
|
-
<span class="text-ink-500 dark:text-ink-400">{statusText}</span>
|
|
215
|
-
</>
|
|
216
|
-
)}
|
|
217
|
-
</>
|
|
218
|
-
) : (
|
|
219
|
-
<>
|
|
220
|
-
{category}
|
|
221
|
-
{status && (
|
|
222
|
-
<>
|
|
223
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
224
|
-
<span class="text-ink-500 dark:text-ink-400">{statusText}</span>
|
|
225
|
-
</>
|
|
226
|
-
)}
|
|
227
|
-
</>
|
|
228
|
-
)}
|
|
229
|
-
</span>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{isCurated && showLabel && (
|
|
234
|
-
<span
|
|
235
|
-
class="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
|
|
236
|
-
title="Hand-curated entry: bestFor, whyListed, caveats, or scores are filled in"
|
|
237
|
-
>
|
|
238
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
239
|
-
<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"/>
|
|
240
|
-
</svg>
|
|
241
|
-
curated
|
|
242
|
-
</span>
|
|
243
|
-
)}
|
|
244
|
-
</header>
|
|
245
|
-
|
|
246
|
-
{/* Description */}
|
|
247
|
-
{item.description && (
|
|
248
|
-
<p
|
|
249
|
-
class="m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400"
|
|
250
|
-
>
|
|
251
|
-
{item.description}
|
|
252
|
-
</p>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
{/* Metadata row: stars · updated · license — the three signals
|
|
256
|
-
that answer "is this alive and usable?" at a glance. */}
|
|
257
|
-
{showMeta && (
|
|
258
|
-
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
259
|
-
{starsLabel !== null && (
|
|
260
|
-
<span class="inline-flex items-center gap-1" title={`${stars ?? 0} stars on GitHub`}>
|
|
261
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
262
|
-
<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"/>
|
|
263
|
-
</svg>
|
|
264
|
-
<span class="font-mono">{starsLabel}</span>
|
|
265
|
-
</span>
|
|
266
|
-
)}
|
|
267
|
-
{updatedAt && (
|
|
268
|
-
<span class="inline-flex items-center gap-1" title={`Last updated ${updatedAt}`}>
|
|
269
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-ink-400">
|
|
270
|
-
<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"/>
|
|
271
|
-
</svg>
|
|
272
|
-
<span class="font-mono">Updated {updatedLabel}</span>
|
|
273
|
-
</span>
|
|
274
|
-
)}
|
|
275
|
-
{license && (
|
|
276
|
-
<span class="inline-flex items-center gap-1" title={`License: ${license}`}>
|
|
277
|
-
<span class="font-mono uppercase tracking-wide text-[10px]">{licenseDisplay(license)}</span>
|
|
278
|
-
</span>
|
|
279
|
-
)}
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
|
|
283
|
-
{/* Platform + stack chips, pushed to the bottom. Shared with
|
|
284
|
-
`IndexRow` / `CollectionRow` so the labelled-row pattern is
|
|
285
|
-
consistent across all card-shaped components. */}
|
|
286
|
-
{showChips && (
|
|
287
|
-
<div class="mt-auto">
|
|
288
|
-
<StackPlatformChips
|
|
289
|
-
stack={visibleStacks[0]}
|
|
290
|
-
platform={visiblePlatforms}
|
|
291
|
-
/>
|
|
292
|
-
{stackOverflow > 0 && (
|
|
293
|
-
<span class="text-2xs text-ink-400 dark:text-ink-600">+{stackOverflow} more stacks</span>
|
|
294
|
-
)}
|
|
295
|
-
</div>
|
|
296
|
-
)}
|
|
297
|
-
|
|
298
|
-
{/* Footer row: View repo + Details links. */}
|
|
299
|
-
<footer class="mt-1 flex items-center justify-between gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
300
|
-
{externalHref && (
|
|
301
|
-
<a
|
|
302
|
-
href={externalHref}
|
|
303
|
-
target="_blank"
|
|
304
|
-
rel="noopener noreferrer"
|
|
305
|
-
data-event="external_repo_click"
|
|
306
|
-
data-event-source="card_view_repo"
|
|
307
|
-
data-event-item={slug}
|
|
308
|
-
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"
|
|
309
|
-
>
|
|
310
|
-
{repoHref ? "View repo" : "Visit source"}
|
|
311
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
312
|
-
<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"/>
|
|
313
|
-
<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"/>
|
|
314
|
-
</svg>
|
|
315
|
-
</a>
|
|
316
|
-
)}
|
|
317
|
-
<a
|
|
318
|
-
href={detailHref}
|
|
319
|
-
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"
|
|
320
|
-
>
|
|
321
|
-
Details →
|
|
322
|
-
</a>
|
|
323
|
-
</footer>
|
|
324
|
-
</article>
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* MinimalAbout — 3 short points + title, used on the about page.
|
|
4
|
-
*
|
|
5
|
-
* A trimmed-down sibling of `WhyThisExists` with a different layout
|
|
6
|
-
* (no icons, no "0X" counter, vertical list) for text-heavy pages.
|
|
7
|
-
*
|
|
8
|
-
* Generic: every point comes through props.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
interface AboutPoint {
|
|
12
|
-
title: string;
|
|
13
|
-
body: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
points?: AboutPoint[];
|
|
18
|
-
title?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
class?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
points,
|
|
25
|
-
title = "About this directory",
|
|
26
|
-
description,
|
|
27
|
-
class: className = "",
|
|
28
|
-
} = Astro.props;
|
|
29
|
-
|
|
30
|
-
const defaultPoints: AboutPoint[] = [
|
|
31
|
-
{
|
|
32
|
-
title: "Curated, not scraped",
|
|
33
|
-
body: "Every entry is hand-checked. If something here, it's because someone looked at the source, ran the build, and decided it was worth your time.",
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
title: "Plain text all the way down",
|
|
37
|
-
body: "Records live in plain YAML, content in plain Markdown, and the static output in plain HTML. No database, no JavaScript framework, no CDN with sticky sessions.",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
title: "Free to fork, free to extend",
|
|
41
|
-
body: "This whole site is one repo. If you want your own copy, fork it. If you want to add a record, open a PR. The directory is the community.",
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const items = points && points.length > 0 ? points : defaultPoints;
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
<section class:list={["py-12 sm:py-16", className]}>
|
|
49
|
-
<div class="mx-auto w-full max-w-narrow px-5 sm:px-7">
|
|
50
|
-
<header class="mb-6 flex flex-col gap-2">
|
|
51
|
-
<h2 class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
52
|
-
{title}
|
|
53
|
-
</h2>
|
|
54
|
-
{description && (
|
|
55
|
-
<p class="m-0 text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
56
|
-
{description}
|
|
57
|
-
</p>
|
|
58
|
-
)}
|
|
59
|
-
</header>
|
|
60
|
-
|
|
61
|
-
<ol class="m-0 flex list-none flex-col gap-5 p-0">
|
|
62
|
-
{items.map((p, i) => (
|
|
63
|
-
<li class="flex gap-4">
|
|
64
|
-
<span
|
|
65
|
-
aria-hidden="true"
|
|
66
|
-
class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 font-mono text-2xs text-ink-500 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-400"
|
|
67
|
-
>
|
|
68
|
-
0{i + 1}
|
|
69
|
-
</span>
|
|
70
|
-
<div>
|
|
71
|
-
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
72
|
-
{p.title}
|
|
73
|
-
</h3>
|
|
74
|
-
<p class="m-0 mt-1 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
75
|
-
{p.body}
|
|
76
|
-
</p>
|
|
77
|
-
</div>
|
|
78
|
-
</li>
|
|
79
|
-
))}
|
|
80
|
-
</ol>
|
|
81
|
-
</div>
|
|
82
|
-
</section>
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* ScoreBars — 4-bar score visualization.
|
|
4
|
-
*
|
|
5
|
-
* Renders a horizontal 4-cell bar per score dimension. Each cell
|
|
6
|
-
* fills based on the score tier (0..4) computed from a 0-100 value.
|
|
7
|
-
* The 5 dimensions are: activity, maturity, learning, contribution,
|
|
8
|
-
* docs. The "overall" bar is optional and rendered last as a
|
|
9
|
-
* composite when `showOverall` is true.
|
|
10
|
-
*
|
|
11
|
-
* Renders nothing when the record has no score block.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { Score } from "@grove-dev/core";
|
|
15
|
-
import { scoreTier, scoreTierLabel, SCORE_LABELS } from "../lib/scores";
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
scores: Score | undefined | null;
|
|
19
|
-
/** Show the "overall" composite bar. Default true. */
|
|
20
|
-
showOverall?: boolean;
|
|
21
|
-
/** Compact mode — no labels next to the bars. */
|
|
22
|
-
compact?: boolean;
|
|
23
|
-
class?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const {
|
|
27
|
-
scores,
|
|
28
|
-
showOverall = true,
|
|
29
|
-
compact = false,
|
|
30
|
-
class: className = "",
|
|
31
|
-
} = Astro.props;
|
|
32
|
-
|
|
33
|
-
const dimensions: (keyof Score)[] = showOverall
|
|
34
|
-
? ["activity", "maturity", "learning", "contribution", "docs", "overall"]
|
|
35
|
-
: ["activity", "maturity", "learning", "contribution", "docs"];
|
|
36
|
-
|
|
37
|
-
function dim(n: number | undefined): number {
|
|
38
|
-
if (typeof n !== "number") return 0;
|
|
39
|
-
return scoreTier(n);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function label(n: number | undefined): string {
|
|
43
|
-
if (typeof n !== "number") return "—";
|
|
44
|
-
return Math.round(n).toString();
|
|
45
|
-
}
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
{scores && (
|
|
49
|
-
<div
|
|
50
|
-
class:list={["grove-score-bars flex flex-col gap-2", className]}
|
|
51
|
-
aria-label="Project scores"
|
|
52
|
-
>
|
|
53
|
-
{dimensions.map((d) => {
|
|
54
|
-
const n = scores[d];
|
|
55
|
-
const tier = dim(n);
|
|
56
|
-
return (
|
|
57
|
-
<div
|
|
58
|
-
class:list={[
|
|
59
|
-
"grove-score-row flex items-center gap-2",
|
|
60
|
-
compact ? "text-2xs" : "text-2xs",
|
|
61
|
-
]}
|
|
62
|
-
title={`${SCORE_LABELS[d]}: ${label(n)} (${scoreTierLabel(typeof n === "number" ? n : 0)})`}
|
|
63
|
-
>
|
|
64
|
-
{!compact && (
|
|
65
|
-
<span class="w-20 flex-shrink-0 font-medium text-ink-700 dark:text-ink-300">
|
|
66
|
-
{SCORE_LABELS[d]}
|
|
67
|
-
</span>
|
|
68
|
-
)}
|
|
69
|
-
<span
|
|
70
|
-
class:list={[
|
|
71
|
-
"grove-score-cells flex gap-0.5",
|
|
72
|
-
compact ? "w-20" : "w-24",
|
|
73
|
-
]}
|
|
74
|
-
role="img"
|
|
75
|
-
aria-label={`${SCORE_LABELS[d]}: ${label(n)}`}
|
|
76
|
-
>
|
|
77
|
-
{[0, 1, 2, 3, 4].map((i) => (
|
|
78
|
-
<span
|
|
79
|
-
class:list={[
|
|
80
|
-
"grove-score-cell h-1.5 flex-1 rounded-sm",
|
|
81
|
-
i < tier
|
|
82
|
-
? d === "overall"
|
|
83
|
-
? "bg-ink-900 dark:bg-ink-100"
|
|
84
|
-
: "bg-ink-700 dark:bg-ink-300"
|
|
85
|
-
: "bg-ink-100 dark:bg-ink-900",
|
|
86
|
-
]}
|
|
87
|
-
></span>
|
|
88
|
-
))}
|
|
89
|
-
</span>
|
|
90
|
-
<span
|
|
91
|
-
class:list={[
|
|
92
|
-
"font-mono text-ink-500 dark:text-ink-400",
|
|
93
|
-
compact ? "ml-1" : "ml-2 w-7 text-right",
|
|
94
|
-
]}
|
|
95
|
-
>
|
|
96
|
-
{label(n)}
|
|
97
|
-
</span>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
package/src/lib/scores.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "@grove-dev/core";
|