@grove-dev/astro 0.5.0-next.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/index.d.ts +0 -2
  4. package/dist/lib/index.d.ts.map +1 -1
  5. package/dist/lib/index.js +0 -2
  6. package/dist/lib/index.js.map +1 -1
  7. package/dist/ui/button.d.ts +45 -0
  8. package/dist/ui/button.d.ts.map +1 -0
  9. package/dist/ui/button.js +82 -0
  10. package/dist/ui/button.js.map +1 -0
  11. package/package.json +8 -5
  12. package/src/components/CardGrid.astro +41 -0
  13. package/src/components/CardIcon.astro +67 -0
  14. package/src/components/CategoryGrid.astro +20 -5
  15. package/src/components/CollectionCard.astro +56 -0
  16. package/src/components/CollectionIndex.astro +21 -26
  17. package/src/components/CollectionPage.astro +36 -13
  18. package/src/components/CollectionRow.astro +62 -192
  19. package/src/components/CollectionTeaser.astro +4 -0
  20. package/src/components/ContributorsGrid.astro +4 -1
  21. package/src/components/DirectoryIndexClient.astro +127 -30
  22. package/src/components/EditorialSummary.astro +6 -5
  23. package/src/components/FilterGroupMenu.astro +24 -10
  24. package/src/components/FilterOptions.astro +6 -1
  25. package/src/components/FinalCta.astro +5 -3
  26. package/src/components/Hero.astro +85 -139
  27. package/src/components/IndexRow.astro +36 -69
  28. package/src/components/LanguageBreakdown.astro +5 -1
  29. package/src/components/OriginalCollection.astro +3 -2
  30. package/src/components/Pagination.astro +7 -20
  31. package/src/components/ProjectCard.astro +345 -0
  32. package/src/components/RecordHeader.astro +111 -82
  33. package/src/components/RecordSection.astro +14 -10
  34. package/src/components/RecordSidebar.astro +68 -20
  35. package/src/components/RefinePanel.astro +41 -62
  36. package/src/components/SmartLensTabs.astro +3 -7
  37. package/src/components/StackGrid.astro +26 -6
  38. package/src/components/SubmissionClient.astro +140 -63
  39. package/src/components/TableOfContents.astro +31 -5
  40. package/src/components/WhyThisExists.astro +1 -1
  41. package/src/index.ts +2 -2
  42. package/src/layouts/BaseLayout.astro +79 -9
  43. package/src/layouts/Header.astro +5 -4
  44. package/src/layouts/SectionHeader.astro +3 -2
  45. package/src/layouts/Seo.astro +28 -17
  46. package/src/layouts/ThemeToggle.astro +23 -6
  47. package/src/lib/index.ts +0 -2
  48. package/src/server/collections.ts +11 -0
  49. package/src/server/contrast.test.ts +82 -0
  50. package/src/server/contrast.ts +117 -0
  51. package/src/server/directory.test.ts +177 -0
  52. package/src/server/directory.ts +328 -206
  53. package/src/server/github-repo.test.ts +88 -0
  54. package/src/server/github-repo.ts +104 -0
  55. package/src/server/index.ts +4 -3
  56. package/src/server/models-home.test.ts +101 -0
  57. package/src/server/models.test.ts +135 -0
  58. package/src/server/models.ts +170 -42
  59. package/src/styles.css +128 -16
  60. package/dist/lib/scores.d.ts +0 -2
  61. package/dist/lib/scores.d.ts.map +0 -1
  62. package/dist/lib/scores.js +0 -2
  63. package/dist/lib/scores.js.map +0 -1
  64. package/src/components/CurationGrid.astro +0 -41
  65. package/src/components/DecisionRow.astro +0 -89
  66. package/src/components/ExploreByCategory.astro +0 -67
  67. package/src/components/ExploreByStack.astro +0 -78
  68. package/src/components/GroveDocumentHead.astro +0 -28
  69. package/src/components/ItemCard.astro +0 -324
  70. package/src/components/MinimalAbout.astro +0 -82
  71. package/src/components/ScoreBars.astro +0 -102
  72. package/src/lib/scores.ts +0 -1
@@ -1,67 +0,0 @@
1
- ---
2
- /**
3
- * ExploreByCategory — compact, count-only browse-by-category row.
4
- *
5
- * Light sibling of CategoryGrid. Pill-style links with a count.
6
- * Generic: takes `{ name, slug, count }` entries.
7
- */
8
-
9
- interface CategoryEntry {
10
- name: string;
11
- slug: string;
12
- count?: number;
13
- }
14
-
15
- interface Props {
16
- categories: CategoryEntry[];
17
- pathPrefix?: string;
18
- title?: string;
19
- description?: string;
20
- class?: string;
21
- }
22
-
23
- const {
24
- categories,
25
- pathPrefix = "/",
26
- title = "Explore by category",
27
- description,
28
- class: className = "",
29
- } = Astro.props;
30
-
31
- function buildHref(slug: string): string {
32
- const base = pathPrefix.replace(/\/$/, "");
33
- return `${base}?category=${encodeURIComponent(slug)}`;
34
- }
35
- ---
36
-
37
- <section class:list={["py-10 sm:py-12", className]}>
38
- <div class="mx-auto w-full max-w-wide px-5 sm:px-7">
39
- <header class="mb-3 flex flex-col gap-1">
40
- <h2 class="m-0 text-[1.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
41
- {title}
42
- </h2>
43
- {description && (
44
- <p class="m-0 text-[0.8125rem] text-ink-500 dark:text-ink-400">
45
- {description}
46
- </p>
47
- )}
48
- </header>
49
- <ul class="m-0 flex list-none flex-wrap gap-2 p-0">
50
- {categories.map((c) => (
51
- <li>
52
- <a
53
- href={buildHref(c.slug)}
54
- 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 items-center gap-1.5 transition-colors hover:border-ink-400 dark:hover:border-ink-600"
55
- data-event="explore_category_click"
56
- data-event-source="explore_category"
57
- >
58
- <span>{c.name}</span>
59
- {typeof c.count === "number" && (
60
- <span class="font-mono text-ink-400">{c.count}</span>
61
- )}
62
- </a>
63
- </li>
64
- ))}
65
- </ul>
66
- </div>
67
- </section>
@@ -1,78 +0,0 @@
1
- ---
2
- /**
3
- * ExploreByStack — compact, count-only browse-by-stack row.
4
- *
5
- * The "explore" companion to StackGrid. Renders a single dense row
6
- * of pill links (one per stack) with a count next to each. No
7
- * cards, no blurbs — meant for the section that fits below the
8
- * primary lens grid.
9
- *
10
- * Generic: takes a list of `{ name, slug, count }` entries. No
11
- * blueprint-specific terms.
12
- */
13
-
14
- interface StackEntry {
15
- name: string;
16
- slug: string;
17
- count?: number;
18
- status?: "live" | "expanding" | "planned";
19
- }
20
-
21
- interface Props {
22
- stacks: StackEntry[];
23
- pathPrefix?: string;
24
- title?: string;
25
- description?: string;
26
- class?: string;
27
- }
28
-
29
- const {
30
- stacks,
31
- pathPrefix = "/",
32
- title = "Explore by stack",
33
- description,
34
- class: className = "",
35
- } = Astro.props;
36
-
37
- function buildHref(slug: string): string {
38
- const base = pathPrefix.replace(/\/$/, "");
39
- return `${base}?stack=${encodeURIComponent(slug)}`;
40
- }
41
- ---
42
-
43
- <section class:list={["py-10 sm:py-12", className]}>
44
- <div class="mx-auto w-full max-w-wide px-5 sm:px-7">
45
- <header class="mb-3 flex flex-col gap-1">
46
- <h2 class="m-0 text-[1.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
47
- {title}
48
- </h2>
49
- {description && (
50
- <p class="m-0 text-[0.8125rem] text-ink-500 dark:text-ink-400">
51
- {description}
52
- </p>
53
- )}
54
- </header>
55
- <ul class="m-0 flex list-none flex-wrap gap-2 p-0">
56
- {stacks.map((s) => (
57
- <li>
58
- <a
59
- href={buildHref(s.slug)}
60
- 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 items-center gap-1.5 transition-colors hover:border-ink-400 dark:hover:border-ink-600"
61
- data-event="explore_stack_click"
62
- data-event-source="explore_stack"
63
- >
64
- <span>{s.name}</span>
65
- {typeof s.count === "number" && (
66
- <span class="font-mono text-ink-400">{s.count}</span>
67
- )}
68
- {s.status && s.status !== "live" && (
69
- <span class="text-[10px] uppercase tracking-wide text-ink-400">
70
- {s.status}
71
- </span>
72
- )}
73
- </a>
74
- </li>
75
- ))}
76
- </ul>
77
- </div>
78
- </section>
@@ -1,28 +0,0 @@
1
- ---
2
- import type { PageDocument } from "@grove-dev/core";
3
- interface Props {
4
- document: PageDocument;
5
- }
6
- const { document: doc } = Astro.props;
7
- ---
8
- <title>{doc.metadata.title}</title>
9
- <meta name="description" content={doc.metadata.description} />
10
- <meta name="robots" content={doc.metadata.robots} />
11
- <link rel="canonical" href={doc.identity.canonical.toString()} />
12
- {doc.identity.hreflang?.map((lang) => (
13
- <link rel="alternate" hreflang={lang} href={doc.identity.canonical.toString()} />
14
- ))}
15
- <meta property="og:title" content={doc.metadata.openGraph.title} />
16
- <meta property="og:description" content={doc.metadata.openGraph.description} />
17
- <meta property="og:url" content={doc.metadata.openGraph.url} />
18
- <meta property="og:image" content={doc.metadata.openGraph.image} />
19
- <meta property="og:type" content={doc.metadata.openGraph.type} />
20
- {doc.metadata.openGraph.siteName && (
21
- <meta property="og:site_name" content={doc.metadata.openGraph.siteName} />
22
- )}
23
- <meta name="twitter:card" content={doc.metadata.twitter.card} />
24
- {doc.metadata.twitter.site && <meta name="twitter:site" content={doc.metadata.twitter.site} />}
25
- {doc.metadata.twitter.creator && <meta name="twitter:creator" content={doc.metadata.twitter.creator} />}
26
- {doc.structuredData.map((node) => (
27
- <script type="application/ld+json" set:html={JSON.stringify(node)} />
28
- ))}
@@ -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";