@grove-dev/astro 0.3.1 → 0.3.4

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 (163) hide show
  1. package/README.md +9 -75
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +11 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.d.ts +1 -26
  6. package/dist/lib/display.d.ts.map +1 -1
  7. package/dist/lib/display.js +1 -103
  8. package/dist/lib/display.js.map +1 -1
  9. package/dist/lib/format.d.ts +1 -28
  10. package/dist/lib/format.d.ts.map +1 -1
  11. package/dist/lib/format.js +1 -71
  12. package/dist/lib/format.js.map +1 -1
  13. package/dist/lib/lenses.d.ts +1 -57
  14. package/dist/lib/lenses.d.ts.map +1 -1
  15. package/dist/lib/lenses.js +1 -198
  16. package/dist/lib/lenses.js.map +1 -1
  17. package/dist/lib/repo.d.ts +1 -19
  18. package/dist/lib/repo.d.ts.map +1 -1
  19. package/dist/lib/repo.js +1 -30
  20. package/dist/lib/repo.js.map +1 -1
  21. package/dist/lib/scores.d.ts +1 -36
  22. package/dist/lib/scores.d.ts.map +1 -1
  23. package/dist/lib/scores.js +1 -75
  24. package/dist/lib/scores.js.map +1 -1
  25. package/dist/lib/search.d.ts +1 -113
  26. package/dist/lib/search.d.ts.map +1 -1
  27. package/dist/lib/search.js +1 -379
  28. package/dist/lib/search.js.map +1 -1
  29. package/dist/lib/taxonomy-counts.d.ts +1 -10
  30. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  31. package/dist/lib/taxonomy-counts.js +1 -21
  32. package/dist/lib/taxonomy-counts.js.map +1 -1
  33. package/package.json +23 -11
  34. package/src/components/ContributorsGrid.astro +3 -3
  35. package/src/components/CurationGrid.astro +2 -2
  36. package/src/components/DirectoryIndexClient.astro +240 -0
  37. package/src/components/FilterGroupMenu.astro +8 -4
  38. package/src/components/FinalCta.astro +41 -0
  39. package/src/components/Hero.astro +15 -18
  40. package/src/components/Icon.astro +35 -7
  41. package/src/components/IndexRow.astro +2 -2
  42. package/src/components/ItemCard.astro +2 -7
  43. package/src/components/RefinePanel.astro +7 -7
  44. package/src/components/SmartLensTabs.astro +9 -5
  45. package/src/components/StackGrid.astro +1 -1
  46. package/src/components/SubmissionClient.astro +133 -0
  47. package/src/index.ts +11 -1
  48. package/src/layouts/BaseLayout.astro +30 -8
  49. package/src/layouts/Footer.astro +25 -56
  50. package/src/layouts/Header.astro +33 -15
  51. package/src/layouts/ThemeToggle.astro +7 -3
  52. package/src/lib/display.ts +1 -111
  53. package/src/lib/format.ts +1 -64
  54. package/src/lib/lenses.ts +1 -220
  55. package/src/lib/repo.ts +1 -32
  56. package/src/lib/scores.ts +1 -90
  57. package/src/lib/search.ts +1 -418
  58. package/src/lib/taxonomy-counts.ts +1 -26
  59. package/{templates/default/src/data/records.ts → src/server/directory.ts} +12 -8
  60. package/src/server/index.ts +2 -0
  61. package/src/server/models.ts +436 -0
  62. package/dist/template-routes.test.d.ts +0 -2
  63. package/dist/template-routes.test.d.ts.map +0 -1
  64. package/dist/template-routes.test.js +0 -63
  65. package/dist/template-routes.test.js.map +0 -1
  66. package/dist/theme.test.d.ts +0 -2
  67. package/dist/theme.test.d.ts.map +0 -1
  68. package/dist/theme.test.js +0 -60
  69. package/dist/theme.test.js.map +0 -1
  70. package/src/template-routes.test.ts +0 -103
  71. package/src/theme.test.ts +0 -76
  72. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  73. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  74. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
  75. package/templates/default/.github/pull_request_template.md +0 -27
  76. package/templates/default/.github/workflows/build.yml +0 -53
  77. package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -46
  78. package/templates/default/.github/workflows/daily-refresh.yml +0 -51
  79. package/templates/default/.github/workflows/sync-contributors.yml +0 -51
  80. package/templates/default/.github/workflows/sync-github-metadata.yml +0 -56
  81. package/templates/default/.github/workflows/validate-data.yml +0 -33
  82. package/templates/default/astro.config.mjs +0 -25
  83. package/templates/default/content/pages/about.md +0 -66
  84. package/templates/default/content/pages/methodology.md +0 -103
  85. package/templates/default/data/records/.gitkeep +0 -0
  86. package/templates/default/data/records/coolify.yml +0 -46
  87. package/templates/default/data/records/gitea.yml +0 -46
  88. package/templates/default/data/records/hoppscotch.yml +0 -47
  89. package/templates/default/data/records/inventree.yml +0 -47
  90. package/templates/default/data/records/label-studio.yml +0 -46
  91. package/templates/default/data/records/logseq.yml +0 -48
  92. package/templates/default/data/records/private-gpt.yml +0 -47
  93. package/templates/default/data/records/wakapi.yml +0 -44
  94. package/templates/default/data/taxonomy/categories.yml +0 -47
  95. package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
  96. package/templates/default/data/taxonomy/platforms.yml +0 -27
  97. package/templates/default/data/taxonomy/stacks.yml +0 -41
  98. package/templates/default/grove.config.ts +0 -36
  99. package/templates/default/package.json +0 -43
  100. package/templates/default/public/icons/platforms/android.svg +0 -1
  101. package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
  102. package/templates/default/public/icons/platforms/apple-light.svg +0 -1
  103. package/templates/default/public/icons/platforms/chrome.svg +0 -1
  104. package/templates/default/public/icons/platforms/chromeos.svg +0 -5
  105. package/templates/default/public/icons/platforms/desktop.svg +0 -4
  106. package/templates/default/public/icons/platforms/embedded.svg +0 -5
  107. package/templates/default/public/icons/platforms/ios.svg +0 -1
  108. package/templates/default/public/icons/platforms/linux.svg +0 -1
  109. package/templates/default/public/icons/platforms/macos.svg +0 -1
  110. package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
  111. package/templates/default/public/icons/platforms/web.svg +0 -5
  112. package/templates/default/public/icons/platforms/windows.svg +0 -6
  113. package/templates/default/public/icons/stacks/android.svg +0 -1
  114. package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
  115. package/templates/default/public/icons/stacks/apple-light.svg +0 -1
  116. package/templates/default/public/icons/stacks/bun.svg +0 -1
  117. package/templates/default/public/icons/stacks/capacitor.svg +0 -14
  118. package/templates/default/public/icons/stacks/clojure.svg +0 -1
  119. package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
  120. package/templates/default/public/icons/stacks/dart.svg +0 -1
  121. package/templates/default/public/icons/stacks/deno.svg +0 -1
  122. package/templates/default/public/icons/stacks/django.svg +0 -1
  123. package/templates/default/public/icons/stacks/docker.svg +0 -1
  124. package/templates/default/public/icons/stacks/firebase.svg +0 -1
  125. package/templates/default/public/icons/stacks/flutter.svg +0 -1
  126. package/templates/default/public/icons/stacks/go.svg +0 -1
  127. package/templates/default/public/icons/stacks/graphql.svg +0 -1
  128. package/templates/default/public/icons/stacks/ionic.svg +0 -4
  129. package/templates/default/public/icons/stacks/java.svg +0 -1
  130. package/templates/default/public/icons/stacks/javascript.svg +0 -1
  131. package/templates/default/public/icons/stacks/kotlin.svg +0 -1
  132. package/templates/default/public/icons/stacks/llm.svg +0 -1
  133. package/templates/default/public/icons/stacks/mongodb.svg +0 -1
  134. package/templates/default/public/icons/stacks/node.js.svg +0 -1
  135. package/templates/default/public/icons/stacks/nodejs.svg +0 -1
  136. package/templates/default/public/icons/stacks/python.svg +0 -1
  137. package/templates/default/public/icons/stacks/rag.svg +0 -7
  138. package/templates/default/public/icons/stacks/react-native.svg +0 -1
  139. package/templates/default/public/icons/stacks/react.svg +0 -1
  140. package/templates/default/public/icons/stacks/rust.svg +0 -1
  141. package/templates/default/public/icons/stacks/solidity.svg +0 -1
  142. package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
  143. package/templates/default/public/icons/stacks/swift.svg +0 -1
  144. package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
  145. package/templates/default/public/icons/stacks/typescript.svg +0 -1
  146. package/templates/default/public/icons/stacks/vue.svg +0 -1
  147. package/templates/default/public/llms-full.txt +0 -83
  148. package/templates/default/public/llms.txt +0 -11
  149. package/templates/default/public/og-image.svg +0 -48
  150. package/templates/default/public/robots.txt +0 -53
  151. package/templates/default/public/sitemap.xml +0 -63
  152. package/templates/default/src/pages/404.astro +0 -64
  153. package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -691
  154. package/templates/default/src/pages/[slug]/index.astro +0 -482
  155. package/templates/default/src/pages/about.astro +0 -189
  156. package/templates/default/src/pages/apps/[recordSlug].astro +0 -34
  157. package/templates/default/src/pages/contributors.astro +0 -175
  158. package/templates/default/src/pages/index.astro +0 -233
  159. package/templates/default/src/pages/sitemap.xml.ts +0 -89
  160. package/templates/default/src/pages/submit.astro +0 -281
  161. package/templates/default/src/styles/global.css +0 -33
  162. package/templates/default/tsconfig.json +0 -9
  163. package/templates/default/wrangler.jsonc +0 -28
@@ -1,691 +0,0 @@
1
- ---
2
- /**
3
- * Detail page — one record at a time.
4
- *
5
- * Layout (top to bottom):
6
- * 1. Breadcrumb — back to the directory index
7
- * 2. AppCard-style header — logo + name + owner + description + CTAs
8
- * 3. ScoreBars — 5-dimension + overall score block
9
- * 4. BestFor / WhyListed / Caveats — 3-column curation grid
10
- * 5. GitHub signals dl — stars, forks, pushed, license, language
11
- * 6. Stack + Platform chips — Icon (with fallback to text)
12
- * 7. Distribution channels — npm/crates/docker/etc. (when present)
13
- * 8. Content markdown — `record.content` (rendered as raw HTML
14
- * body) when present
15
- * 9. JSON-LD SoftwareSourceCode — for SEO + AI crawlers
16
- *
17
- * Generic across the 3 record kinds (project / resource / entity) —
18
- * non-project fields simply render nothing. The URL is dynamic on
19
- * both `[slug]` (blueprint route, e.g. `projects`/`resources`/
20
- * `entities`) and `[recordSlug]` (the record slug), so a single page
21
- * file serves every blueprint and every record.
22
- */
23
- import siteConfig from "../../../data/generated/site-config.json";
24
- import {
25
- fullRecords,
26
- projectBySlug,
27
- resourceBySlug,
28
- entityBySlug,
29
- indexSlug,
30
- getContentHtml,
31
- } from "../../data/records";
32
- import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
33
- import Icon from "@grove-dev/astro/components/Icon.astro";
34
- import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
35
- import CurationGrid from "@grove-dev/astro/components/CurationGrid.astro";
36
- import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
37
- import { formatRelative, formatStars } from "@grove-dev/astro";
38
- import { statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
39
- import type { LabelId } from "@grove-dev/astro";
40
- import type { ProjectRecord } from "@grove-dev/core";
41
-
42
- export function getStaticPaths() {
43
- // Walk every record and emit a path for it. The directory slug
44
- // is read from the static site-config import at the top of this
45
- // file (Vite resolves the JSON at build time). This template
46
- // ships with the `project-directory` blueprint, so the slug is
47
- // always `projects`; swapping in a different blueprint means
48
- // changing the blueprint config that drives this value.
49
- const dirSlug = siteConfig.blueprintConfig?.routeSlug ?? "projects";
50
- return fullRecords.map((record) => ({
51
- params: { slug: dirSlug, recordSlug: record.slug },
52
- }));
53
- }
54
-
55
- const slug = Astro.params.slug ?? indexSlug();
56
- const recordSlug = Astro.params.recordSlug ?? "";
57
- const project = projectBySlug(recordSlug);
58
- const resource = resourceBySlug(recordSlug);
59
- const entity = entityBySlug(recordSlug);
60
- const record = project ?? resource ?? entity;
61
-
62
- if (!record) {
63
- return Astro.redirect(`/${slug}`);
64
- }
65
-
66
- const isProject = record.kind === "project";
67
- const proj: ProjectRecord | null = project ?? null;
68
-
69
- const name =
70
- record.kind === "resource" ? (record as { title: string }).title : (record as { name: string }).name;
71
-
72
- const title = `${name} — ${siteConfig.name}`;
73
- const description = record.description ?? `${name} on ${siteConfig.name}`;
74
- const itemSingular = siteConfig.blueprintConfig?.labelSingular ?? record.kind;
75
-
76
- const repoUrl = proj?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
77
- const homepageUrl = record.links?.website ?? "";
78
- const logoUrl = proj?.logoUrl;
79
- const stacks = proj?.stacks ?? (proj?.stack ? [proj.stack] : []);
80
- const platforms = proj?.platforms ?? [];
81
- const github = proj?.github?.repository;
82
- const stars = github?.stargazers_count ?? 0;
83
- const forks = github?.forks_count ?? 0;
84
- const language = github?.language ?? null;
85
- const licenseSpdx = github?.license?.spdx_id ?? null;
86
- const pushedAt = github?.pushed_at ?? null;
87
- const starsLabel = formatStars(stars);
88
- const pushedLabel = formatRelative(pushedAt);
89
-
90
- const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
91
- const avatarSrc = logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
92
- const initials = (name || "").replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase();
93
-
94
- const bestFor = proj?.bestFor ?? [];
95
- const whyListed = proj?.whyListed ?? [];
96
- const caveats = proj?.caveats ?? [];
97
- const distribution = proj?.distribution?.channels ?? [];
98
- const scores = proj?.scores ?? null;
99
- const curationLabels = record.curation?.labels ?? [];
100
- const tags = record.tags ?? [];
101
- const healthStatus = proj?.health?.status;
102
- const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
103
-
104
- // Map each known label to a static Tailwind class string so every
105
- // utility is visible to Tailwind's source scanner. Unknown/legacy
106
- // labels use the neutral secondary badge treatment.
107
- const labelClassMap: Record<LabelId, string> = {
108
- new: "!bg-emerald-50 !text-emerald-700 dark:!bg-emerald-950/80 dark:!text-emerald-300",
109
- hot: "!bg-orange-50 !text-orange-800 dark:!bg-orange-950/80 dark:!text-orange-300",
110
- mature: "!bg-blue-50 !text-blue-700 dark:!bg-blue-950/80 dark:!text-blue-300",
111
- featured: "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",
112
- };
113
-
114
- // ── Optional content markdown (record.content is a path under
115
- // content/records/<slug>.md) ────────────────────────────────────
116
- // The read + marked.parse + sanitize-html pipeline lives in
117
- // `data/records.ts` and runs once at module load. The page just
118
- // looks up the pre-sanitized HTML by record slug; no node:fs
119
- // import needed here.
120
- const contentHtml: string | null = isProject ? getContentHtml(recordSlug) : null;
121
-
122
- // ── Detail-page decision-signal data (5 sections) ────────────
123
- // Hoist out of the template body so the JSX stays free of
124
- // complex TypeScript type assertions.
125
- type MonthlyCommit = number | { month: string; commits: number };
126
- type ProjectExtras = {
127
- github?: {
128
- activity?: { monthlyCommits?: MonthlyCommit[] };
129
- files?: Record<string, boolean>;
130
- languages?: Record<string, number>;
131
- };
132
- health?: { status?: string; tier?: string };
133
- };
134
- const extras = record as ProjectExtras;
135
- const monthlyCommits = (extras.github?.activity?.monthlyCommits ?? []).map(
136
- (it): { month: string; commits: number } =>
137
- typeof it === "number" ? { month: "", commits: it } : { month: it.month ?? "", commits: it.commits ?? 0 },
138
- );
139
- const maxMonthlyCommits = Math.max(1, ...monthlyCommits.map((i) => i.commits));
140
- const contributionFiles = extras.github?.files ?? {};
141
- const contributionLabels: Record<string, string> = {
142
- readme: "README",
143
- contributing: "Contributing",
144
- security: "Security",
145
- issueTemplates: "Issue templates",
146
- pullRequestTemplate: "PR template",
147
- };
148
- const contributionSignals = (Object.keys(contributionLabels) as Array<keyof typeof contributionLabels>)
149
- .map((k) => ({ key: k, label: contributionLabels[k], ok: contributionFiles[k] }))
150
- .filter((s) => s.ok !== undefined);
151
- const languages = extras.github?.languages
152
- ? Object.entries(extras.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
153
- : [];
154
- const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, b]) => sum + b, 0));
155
-
156
- // ── Kind-aware JSON-LD ─────────────────────────────────────────
157
- // Mirrors the openapps field set (10+ fields for projects):
158
- // name, headline, description, url, codeRepository, sameAs,
159
- // programmingLanguage, license, applicationCategory,
160
- // operatingSystem, keywords, dateCreated, dateModified,
161
- // interactionStatistic, author, isAccessibleForFree.
162
- let jsonLd: Record<string, unknown>;
163
- if (isProject && proj) {
164
- const sameAs = [repoUrl, homepageUrl].filter((value): value is string =>
165
- typeof value === "string" && value.length > 0,
166
- );
167
- const keywords = (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined;
168
- const operatingSystem = (proj.platforms ?? []).length > 0
169
- ? (proj.platforms ?? []).join(", ")
170
- : undefined;
171
- const applicationCategory = (record.category ?? "") || undefined;
172
- const dateCreated = (record as { curation?: { reviewedAt?: string } }).curation?.reviewedAt;
173
- const dateModified = pushedAt ?? dateCreated;
174
- const interactionStatistic = stars > 0
175
- ? {
176
- "@type": "InteractionCounter",
177
- interactionType: { "@type": "LikeAction" },
178
- userInteractionCount: stars,
179
- }
180
- : undefined;
181
- jsonLd = {
182
- "@context": "https://schema.org",
183
- "@type": "SoftwareSourceCode",
184
- name,
185
- headline: name,
186
- description: record.description ?? undefined,
187
- url: repoUrl || homepageUrl || undefined,
188
- codeRepository: repoUrl || undefined,
189
- sameAs: sameAs.length > 0 ? sameAs : undefined,
190
- programmingLanguage: language ?? undefined,
191
- license: licenseSpdx ?? undefined,
192
- applicationCategory,
193
- operatingSystem,
194
- keywords,
195
- dateCreated: dateCreated ?? undefined,
196
- dateModified: dateModified ?? undefined,
197
- interactionStatistic,
198
- author: ownerRepo
199
- ? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
200
- : undefined,
201
- isAccessibleForFree: true,
202
- };
203
- } else if (record.kind === "resource") {
204
- const resourceSchemaType: Record<string, string> = {
205
- article: "Article",
206
- book: "Book",
207
- course: "Course",
208
- podcast: "PodcastSeries",
209
- video: "VideoObject",
210
- };
211
- jsonLd = {
212
- "@context": "https://schema.org",
213
- "@type": resourceSchemaType[record.type] ?? "CreativeWork",
214
- name,
215
- headline: name,
216
- description: record.description || undefined,
217
- url: homepageUrl || undefined,
218
- author: record.author ? { "@type": "Person", name: record.author } : undefined,
219
- datePublished: record.publishedAt || undefined,
220
- keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
221
- isAccessibleForFree: true,
222
- };
223
- } else if (entity) {
224
- jsonLd = {
225
- "@context": "https://schema.org",
226
- "@type": entity.type === "person" ? "Person" : "Organization",
227
- name,
228
- headline: name,
229
- description: entity.description || undefined,
230
- url: homepageUrl || undefined,
231
- foundingDate: entity.founded || undefined,
232
- location: entity.location || undefined,
233
- keywords: (entity.tags ?? []).length > 0 ? (entity.tags ?? []).join(", ") : undefined,
234
- };
235
- } else {
236
- jsonLd = {};
237
- }
238
- ---
239
-
240
- <BaseLayout
241
- title={title}
242
- description={description}
243
- site={siteConfig}
244
- type="article"
245
- jsonLd={jsonLd}
246
- submitHref="/submit"
247
- submitLabel={`Submit ${name}`}
248
- >
249
- <article class="py-12 sm:py-16">
250
- <div class="mx-auto w-full max-w-wide px-5 sm:px-7">
251
- <nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
252
- <a href={`/${slug}`} class="hover:text-ink-900 dark:hover:text-ink-100 capitalize">
253
- {slug}
254
- </a>
255
- <span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
256
- <span class="text-ink-700 dark:text-ink-300">{name}</span>
257
- </nav>
258
-
259
- {/* ── AppCard-style header ──────────────────────────────── */}
260
- <header class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
261
- <div class="flex-shrink-0">
262
- {avatarSrc ? (
263
- <img
264
- src={avatarSrc}
265
- alt=""
266
- width="64"
267
- height="64"
268
- loading="lazy"
269
- decoding="async"
270
- class="h-16 w-16 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950"
271
- />
272
- ) : (
273
- <span
274
- aria-hidden="true"
275
- class="inline-flex h-16 w-16 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-2xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
276
- >
277
- {initials || "·"}
278
- </span>
279
- )}
280
- </div>
281
-
282
- <div class="min-w-0 flex-1">
283
- <div class="flex flex-wrap items-center gap-2">
284
- <span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:text-ink-300">
285
- {record.kind}
286
- </span>
287
- {record.category && (
288
- <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">
289
- {record.category}
290
- </span>
291
- )}
292
- {curationLabels.map((l) => (
293
- <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 ${labelClassMap[l as LabelId] ?? ""}`} title={`Curated: ${labelDisplay(l) ?? l}`}>
294
- {labelDisplay(l) ?? prettySlug(l)}
295
- </span>
296
- ))}
297
- {record.curation?.reviewed && (
298
- <span
299
- 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"
300
- title="A human curator reviewed this record. (No reviewer name is shown — the framework is reviewer-agnostic; consumers can wire the curation.by field into their own badge component.)"
301
- >
302
- <svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
303
- <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" />
304
- </svg>
305
- Curator reviewed
306
- </span>
307
- )}
308
- {healthLabel && (
309
- <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-amber-50 !text-amber-800 dark:!bg-amber-950/80 dark:!text-amber-300">{healthLabel}</span>
310
- )}
311
- </div>
312
-
313
- <h1 class="m-0 mt-2 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
314
- {name}
315
- </h1>
316
-
317
- {ownerRepo && (
318
- <p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
319
- <a
320
- href={`https://github.com/${ownerRepo.owner}`}
321
- target="_blank"
322
- rel="noopener noreferrer"
323
- class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
324
- >
325
- @{ownerRepo.owner}
326
- </a>
327
- <span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
328
- <a
329
- href={repoUrl}
330
- target="_blank"
331
- rel="noopener noreferrer"
332
- class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
333
- >
334
- {ownerRepo.repo}
335
- </a>
336
- </p>
337
- )}
338
-
339
- {record.description && (
340
- <p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
341
- {record.description}
342
- </p>
343
- )}
344
-
345
- <div class="mt-5 flex flex-wrap gap-2">
346
- {repoUrl && (
347
- <a
348
- href={repoUrl}
349
- target="_blank"
350
- rel="noopener noreferrer"
351
- 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"
352
- >
353
- View on GitHub →
354
- </a>
355
- )}
356
- {homepageUrl && (
357
- <a
358
- href={homepageUrl}
359
- target="_blank"
360
- rel="noopener noreferrer"
361
- 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 border-border bg-secondary text-foreground hover:bg-accent"
362
- >
363
- Homepage
364
- </a>
365
- )}
366
- </div>
367
- </div>
368
- </header>
369
-
370
- {/* ── Score bars ───────────────────────────────────────── */}
371
- {proj && (
372
- <section class="mt-8 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-6" aria-labelledby="scores-heading">
373
- <h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
374
- Scores
375
- </h2>
376
- <p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
377
- A five-dimension read of how likely this project is to satisfy a curious developer.
378
- </p>
379
- <div class="mt-4">
380
- <ScoreBars scores={scores} />
381
- </div>
382
- </section>
383
- )}
384
-
385
- {/* ── BestFor / WhyListed / Caveats grid ──────────────── */}
386
- {isProject && (
387
- <CurationGrid
388
- bestFor={bestFor}
389
- whyListed={whyListed}
390
- caveats={caveats}
391
- class="mt-8"
392
- />
393
- )}
394
-
395
- {/* ── GitHub signals dl ────────────────────────────────── */}
396
- {isProject && github && (
397
- <section class="mt-8 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-6" aria-labelledby="signals-heading">
398
- <h2 id="signals-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
399
- GitHub signals
400
- </h2>
401
- <dl
402
- class="mt-4 grid grid-cols-2 gap-x-6 gap-y-3 text-sm sm:grid-cols-3"
403
- aria-label="Repository signals"
404
- >
405
- <div>
406
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
407
- Stars
408
- </dt>
409
- <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
410
- {starsLabel ?? "—"}
411
- </dd>
412
- </div>
413
- <div>
414
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
415
- Forks
416
- </dt>
417
- <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
418
- {forks}
419
- </dd>
420
- </div>
421
- <div>
422
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
423
- Last push
424
- </dt>
425
- <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
426
- {pushedLabel}
427
- </dd>
428
- </div>
429
- {language && (
430
- <div>
431
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
432
- Language
433
- </dt>
434
- <dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{language}</dd>
435
- </div>
436
- )}
437
- {licenseSpdx && (
438
- <div>
439
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
440
- License
441
- </dt>
442
- <dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
443
- </div>
444
- )}
445
- {pushedAt && (
446
- <div>
447
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
448
- Updated
449
- </dt>
450
- <dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
451
- {pushedAt.slice(0, 10)}
452
- </dd>
453
- </div>
454
- )}
455
- </dl>
456
- </section>
457
- )}
458
-
459
- {/* ── Decision signals (3-col) ──────────────────────────── */}
460
- {isProject && github && (
461
- <section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3" aria-label="Decision signals">
462
- {/* Repository health card */}
463
- <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
464
- <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
465
- Repository health
466
- </h3>
467
- <dl class="mt-4 grid gap-3 text-sm">
468
- <div class="flex items-center justify-between gap-3">
469
- <dt class="text-ink-500 dark:text-ink-400">Status</dt>
470
- <dd class="font-medium text-ink-900 dark:text-ink-100">
471
- {healthLabel ?? "—"}
472
- </dd>
473
- </div>
474
- <div class="flex items-center justify-between gap-3">
475
- <dt class="text-ink-500 dark:text-ink-400">Stars</dt>
476
- <dd class="font-mono text-ink-900 dark:text-ink-100">
477
- {starsLabel ?? "—"}
478
- </dd>
479
- </div>
480
- <div class="flex items-center justify-between gap-3">
481
- <dt class="text-ink-500 dark:text-ink-400">Forks</dt>
482
- <dd class="font-mono text-ink-900 dark:text-ink-100">
483
- {forks}
484
- </dd>
485
- </div>
486
- <div class="flex items-center justify-between gap-3">
487
- <dt class="text-ink-500 dark:text-ink-400">Open issues</dt>
488
- <dd class="font-mono text-ink-900 dark:text-ink-100">
489
- {github?.open_issues_count ?? "—"}
490
- </dd>
491
- </div>
492
- {licenseSpdx && (
493
- <div class="flex items-center justify-between gap-3">
494
- <dt class="text-ink-500 dark:text-ink-400">License</dt>
495
- <dd class="font-mono text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
496
- </div>
497
- )}
498
- </dl>
499
- </div>
500
-
501
- {/* Activity bar chart */}
502
- <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
503
- <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
504
- Activity
505
- </h3>
506
- {monthlyCommits.length > 0 ? (
507
- <figure class="mt-4 m-0">
508
- <div
509
- class="flex h-28 items-end gap-1.5"
510
- role="img"
511
- aria-label="Monthly commits over the last 12 months"
512
- >
513
- {monthlyCommits.map((item) => (
514
- <div class="flex min-w-0 flex-1 flex-col items-center gap-1">
515
- <div
516
- class="w-full rounded-sm bg-ink-700 dark:bg-ink-200"
517
- style={`height: ${Math.max(8, Math.round((item.commits / maxMonthlyCommits) * 96))}px`}
518
- title={`${item.month || "month"}: ${item.commits} commits`}
519
- />
520
- <span class="max-w-full truncate text-[10px] text-ink-500 dark:text-ink-400">
521
- {item.month ? item.month.slice(5) : item.commits}
522
- </span>
523
- </div>
524
- ))}
525
- </div>
526
- <figcaption class="sr-only">
527
- Monthly commit count over the last 12 months
528
- </figcaption>
529
- </figure>
530
- ) : (
531
- <p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
532
- Monthly commit history is not yet populated for this {itemSingular}.
533
- </p>
534
- )}
535
- </div>
536
-
537
- {/* Contribution readiness chips */}
538
- <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
539
- <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
540
- Contribution readiness
541
- </h3>
542
- {contributionSignals.length > 0 ? (
543
- <ul class="mt-4 m-0 grid list-none gap-2 p-0 text-sm">
544
- {contributionSignals.map((signal) => (
545
- <li class="flex items-center justify-between gap-3">
546
- <span class="text-ink-700 dark:text-ink-200">{signal.label}</span>
547
- <span class={signal.ok ? "text-emerald-700 dark:text-emerald-400" : "text-ink-400 dark:text-ink-600"}>
548
- {signal.ok ? "Found" : "Missing"}
549
- </span>
550
- </li>
551
- ))}
552
- </ul>
553
- ) : (
554
- <p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
555
- Contribution files have not been synced yet.
556
- </p>
557
- )}
558
- </div>
559
- </section>
560
- )}
561
-
562
- {/* ── Language mix ─────────────────────────────────────── */}
563
- {isProject && languages.length > 0 && (
564
- <section class="mt-4 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
565
- <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
566
- Language mix
567
- </h3>
568
- <dl class="mt-4 grid gap-2 text-sm">
569
- {languages.map(([language, bytes]) => {
570
- const pct = Math.round((bytes / totalLanguageBytes) * 100);
571
- return (
572
- <div class="grid grid-cols-[8rem_minmax(0,1fr)_3rem] items-center gap-3">
573
- <dt class="truncate text-ink-700 dark:text-ink-200">{language}</dt>
574
- <dd class="h-2 overflow-hidden rounded-sm bg-ink-200 dark:bg-ink-800">
575
- <div class="h-full bg-ink-700 dark:bg-ink-200" style={`width: ${pct}%`} />
576
- </dd>
577
- <dd class="text-right font-mono text-2xs text-ink-500 dark:text-ink-400">{pct}%</dd>
578
- </div>
579
- );
580
- })}
581
- </dl>
582
- </section>
583
- )}
584
-
585
- {/* ── Stack + Platform chips ───────────────────────────── */}
586
- {isProject && (stacks.length > 0 || platforms.length > 0) && (
587
- <section class="mt-8" aria-labelledby="stacks-heading">
588
- <h2 id="stacks-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
589
- Stack & platforms
590
- </h2>
591
- <div class="mt-4 flex flex-col gap-3">
592
- {stacks.length > 0 && (
593
- <div class="flex flex-wrap items-center gap-2">
594
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
595
- Stack
596
- </span>
597
- {stacks.map((s) => (
598
- <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 items-center gap-1.5">
599
- <Icon name={s} category="stack" size={14} />
600
- {s}
601
- </span>
602
- ))}
603
- </div>
604
- )}
605
- {platforms.length > 0 && (
606
- <div class="flex flex-wrap items-center gap-2">
607
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
608
- Platform
609
- </span>
610
- {platforms.map((p) => (
611
- <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 items-center gap-1.5">
612
- <Icon name={p} category="platform" size={14} />
613
- {p}
614
- </span>
615
- ))}
616
- </div>
617
- )}
618
- </div>
619
- </section>
620
- )}
621
-
622
- {/* ── Tags ─────────────────────────────────────────────── */}
623
- {tags.length > 0 && (
624
- <section class="mt-8" aria-labelledby="tags-heading">
625
- <h2 id="tags-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
626
- Tags
627
- </h2>
628
- <ul class="mt-4 m-0 flex list-none flex-wrap gap-1.5 p-0">
629
- {tags.map((t) => (
630
- <li>
631
- <a
632
- href={`/${slug}?q=${encodeURIComponent(t)}`}
633
- 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"
634
- >
635
- {t}
636
- </a>
637
- </li>
638
- ))}
639
- </ul>
640
- </section>
641
- )}
642
-
643
- {/* ── Distribution channels ────────────────────────────── */}
644
- {distribution.length > 0 && (
645
- <section class="mt-8" aria-labelledby="dist-heading">
646
- <h2 id="dist-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
647
- Distribution channels
648
- </h2>
649
- <ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
650
- {distribution.map((c) => (
651
- <li class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-wrap items-baseline justify-between gap-2 p-3">
652
- <div class="flex flex-col">
653
- <span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
654
- {c.label ?? c.type}
655
- {c.verified && (
656
- <span class="ml-2 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-emerald-50 !text-emerald-700 dark:!bg-emerald-950/80 dark:!text-emerald-300">Verified</span>
657
- )}
658
- </span>
659
- {c.notes && (
660
- <span class="text-xs text-ink-500 dark:text-ink-400">{c.notes}</span>
661
- )}
662
- </div>
663
- <a
664
- href={c.url}
665
- target="_blank"
666
- rel="noopener noreferrer"
667
- 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 border-border bg-secondary text-foreground hover:bg-accent"
668
- >
669
- {c.platform ?? c.type} →
670
- </a>
671
- </li>
672
- ))}
673
- </ul>
674
- </section>
675
- )}
676
-
677
- {/* ── Content markdown ─────────────────────────────────── */}
678
- {contentHtml && (
679
- <section class="mt-8" aria-labelledby="content-heading">
680
- <h2 id="content-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
681
- Notes
682
- </h2>
683
- <div
684
- class="grove-prose mt-4"
685
- set:html={contentHtml}
686
- />
687
- </section>
688
- )}
689
- </div>
690
- </article>
691
- </BaseLayout>