@grove-dev/astro 0.2.19 → 0.3.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/dist/template-routes.test.d.ts +2 -0
  3. package/dist/template-routes.test.d.ts.map +1 -0
  4. package/dist/template-routes.test.js +63 -0
  5. package/dist/template-routes.test.js.map +1 -0
  6. package/dist/theme.test.js +19 -0
  7. package/dist/theme.test.js.map +1 -1
  8. package/package.json +4 -4
  9. package/src/components/CategoryGrid.astro +2 -11
  10. package/src/components/ContributorsGrid.astro +2 -11
  11. package/src/components/CurationGrid.astro +1 -1
  12. package/src/components/DecisionRow.astro +1 -1
  13. package/src/components/ExploreByCategory.astro +3 -3
  14. package/src/components/ExploreByStack.astro +3 -3
  15. package/src/components/FilterGroupMenu.astro +5 -5
  16. package/src/components/Hero.astro +8 -8
  17. package/src/components/Icon.astro +56 -53
  18. package/src/components/IndexRow.astro +5 -5
  19. package/src/components/ItemCard.astro +5 -16
  20. package/src/components/MinimalAbout.astro +2 -2
  21. package/src/components/OriginalCollection.astro +4 -4
  22. package/src/components/Pagination.astro +2 -2
  23. package/src/components/RecordSection.astro +1 -1
  24. package/src/components/RefinePanel.astro +1 -1
  25. package/src/components/SmartLensTabs.astro +3 -3
  26. package/src/components/StackGrid.astro +6 -15
  27. package/src/components/WhyThisExists.astro +3 -3
  28. package/src/layouts/Container.astro +4 -4
  29. package/src/layouts/Footer.astro +3 -3
  30. package/src/layouts/Header.astro +5 -5
  31. package/src/layouts/ThemeToggle.astro +1 -1
  32. package/src/styles.css +33 -339
  33. package/src/template-routes.test.ts +103 -0
  34. package/src/theme.test.ts +24 -0
  35. package/templates/default/.github/workflows/build.yml +1 -3
  36. package/templates/default/.github/workflows/cleanup-stale-records.yml +1 -3
  37. package/templates/default/.github/workflows/daily-refresh.yml +1 -3
  38. package/templates/default/.github/workflows/sync-contributors.yml +2 -4
  39. package/templates/default/.github/workflows/sync-github-metadata.yml +1 -3
  40. package/templates/default/.github/workflows/validate-data.yml +1 -3
  41. package/templates/default/grove.config.ts +1 -1
  42. package/templates/default/package.json +9 -4
  43. package/templates/default/public/icons/stacks/rag.svg +7 -0
  44. package/templates/default/public/llms-full.txt +63 -186
  45. package/templates/default/public/llms.txt +6 -107
  46. package/templates/default/public/sitemap.xml +10 -10
  47. package/templates/default/src/data/records.ts +80 -2
  48. package/templates/default/src/pages/404.astro +5 -5
  49. package/templates/default/src/pages/[slug]/[recordSlug].astro +36 -38
  50. package/templates/default/src/pages/[slug]/index.astro +223 -19
  51. package/templates/default/src/pages/about.astro +17 -10
  52. package/templates/default/src/pages/apps/[recordSlug].astro +2 -1
  53. package/templates/default/src/pages/contributors.astro +8 -17
  54. package/templates/default/src/pages/index.astro +11 -3
  55. package/templates/default/src/pages/sitemap.xml.ts +3 -3
  56. package/templates/default/src/pages/submit.astro +28 -36
  57. package/templates/default/tsconfig.json +9 -0
  58. package/templates/default/scripts/build-llms.mjs +0 -123
  59. package/templates/default/scripts/build-records-json.mjs +0 -27
  60. package/templates/default/scripts/build-sitemap.mjs +0 -20
  61. package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
  62. package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
  63. package/templates/default/scripts/fetch-icons.mjs +0 -137
  64. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
  65. package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
  66. package/templates/default/scripts/refresh-records-activity.mjs +0 -24
  67. package/templates/default/scripts/repair-license-format.mjs +0 -72
  68. package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
  69. package/templates/default/scripts/seed-from-github.mjs +0 -62
  70. package/templates/default/scripts/sync-contributors.mjs +0 -145
  71. package/templates/default/scripts/sync-github-metadata.mjs +0 -28
  72. package/templates/default/scripts/validate-records.mjs +0 -27
@@ -2,61 +2,61 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://example.com/</loc>
5
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
5
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
6
6
  <changefreq>daily</changefreq>
7
7
  <priority>1.0</priority>
8
8
  </url>
9
9
  <url>
10
10
  <loc>https://example.com/projects</loc>
11
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
11
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
12
12
  <changefreq>daily</changefreq>
13
13
  <priority>0.9</priority>
14
14
  </url>
15
15
  <url>
16
16
  <loc>https://example.com/projects/coolify</loc>
17
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
17
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
18
18
  <changefreq>weekly</changefreq>
19
19
  <priority>0.7</priority>
20
20
  </url>
21
21
  <url>
22
22
  <loc>https://example.com/projects/gitea</loc>
23
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
23
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
24
24
  <changefreq>weekly</changefreq>
25
25
  <priority>0.7</priority>
26
26
  </url>
27
27
  <url>
28
28
  <loc>https://example.com/projects/hoppscotch</loc>
29
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
29
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
30
30
  <changefreq>weekly</changefreq>
31
31
  <priority>0.7</priority>
32
32
  </url>
33
33
  <url>
34
34
  <loc>https://example.com/projects/inventree</loc>
35
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
35
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
36
36
  <changefreq>weekly</changefreq>
37
37
  <priority>0.7</priority>
38
38
  </url>
39
39
  <url>
40
40
  <loc>https://example.com/projects/label-studio</loc>
41
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
41
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
42
42
  <changefreq>weekly</changefreq>
43
43
  <priority>0.7</priority>
44
44
  </url>
45
45
  <url>
46
46
  <loc>https://example.com/projects/logseq</loc>
47
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
47
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
48
48
  <changefreq>weekly</changefreq>
49
49
  <priority>0.7</priority>
50
50
  </url>
51
51
  <url>
52
52
  <loc>https://example.com/projects/private-gpt</loc>
53
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
53
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
54
54
  <changefreq>weekly</changefreq>
55
55
  <priority>0.7</priority>
56
56
  </url>
57
57
  <url>
58
58
  <loc>https://example.com/projects/wakapi</loc>
59
- <lastmod>2026-06-22T14:50:53.941Z</lastmod>
59
+ <lastmod>2026-07-01T16:50:32.236Z</lastmod>
60
60
  <changefreq>weekly</changefreq>
61
61
  <priority>0.7</priority>
62
62
  </url>
@@ -38,6 +38,7 @@ import { fileURLToPath } from "node:url";
38
38
  import { dirname, resolve } from "node:path";
39
39
  import { marked } from "marked";
40
40
  import sanitizeHtml from "sanitize-html";
41
+ import { prettySlug } from "@grove-dev/astro";
41
42
  import type {
42
43
  ProjectRecord,
43
44
  ResourceRecord,
@@ -79,12 +80,39 @@ interface SiteConfigPayload {
79
80
  labelSingular?: string;
80
81
  labelPlural?: string;
81
82
  };
83
+ taxonomy?: Partial<
84
+ Record<
85
+ "categories" | "stacks" | "platforms" | "distributionChannels",
86
+ Array<{ id: string; name: string }>
87
+ >
88
+ >;
82
89
  name?: string;
83
90
  }
84
91
 
85
- const fullRecordsRaw: Resource[] = (fullPayload as FullPayload).records ?? [];
86
- const indexRecordsRaw: IndexRecord[] = (indexPayload as IndexPayload).records ?? [];
92
+ const fullRecordsRaw: Resource[] =
93
+ (fullPayload as unknown as FullPayload).records ?? [];
94
+ const indexRecordsRaw: IndexRecord[] =
95
+ (indexPayload as unknown as IndexPayload).records ?? [];
87
96
  const siteConfigRaw: SiteConfigPayload = siteConfigPayload as SiteConfigPayload;
97
+ const taxonomyMaps = Object.fromEntries(
98
+ Object.entries(siteConfigRaw.taxonomy ?? {}).map(([kind, entries]) => [
99
+ kind,
100
+ new Map((entries ?? []).map((entry) => [entry.id, entry.name])),
101
+ ]),
102
+ ) as Partial<
103
+ Record<
104
+ "categories" | "stacks" | "platforms" | "distributionChannels",
105
+ Map<string, string>
106
+ >
107
+ >;
108
+ export const taxonomy = siteConfigRaw.taxonomy ?? {};
109
+
110
+ export function taxonomyLabel(
111
+ kind: "categories" | "stacks" | "platforms" | "distributionChannels",
112
+ id: string,
113
+ ): string {
114
+ return taxonomyMaps[kind]?.get(id) ?? prettySlug(id);
115
+ }
88
116
 
89
117
  /** Full records (every record, all visibility). Use this for the
90
118
  * detail page (where you need `content`, `bestFor`, `whyListed`,
@@ -224,6 +252,10 @@ export const fullItems: Resource[] = (() => {
224
252
  return fullRecords.filter((r) => r.kind === blueprintKind);
225
253
  })();
226
254
 
255
+ export const fullProjects: ProjectRecord[] = fullRecords.filter(
256
+ (record): record is ProjectRecord => record.kind === "project",
257
+ );
258
+
227
259
  // ──────────────────────────────────────────────────────────────────────
228
260
  // Markdown content rendering (sanitized)
229
261
  // ──────────────────────────────────────────────────────────────────────
@@ -316,3 +348,49 @@ for (const r of fullRecords) {
316
348
  export function getContentHtml(slug: string): string | null {
317
349
  return contentHtmlBySlug.get(slug) ?? null;
318
350
  }
351
+
352
+ /**
353
+ * Sanitized Markdown for a consumer-authored page under
354
+ * `content/pages/<page>.md`. Default template pages use this to
355
+ * accept custom copy while keeping Grove's layout and components.
356
+ */
357
+ export function getPageContentHtml(page: string): string | null {
358
+ const candidates = [
359
+ resolve(here, "..", "..", "content", "pages", `${page}.md`),
360
+ resolve(here, "..", "..", "..", "content", "pages", `${page}.md`),
361
+ resolve(process.cwd(), "content", "pages", `${page}.md`),
362
+ ];
363
+ const path = candidates.find((candidate) => existsSync(candidate));
364
+ if (!path) return null;
365
+
366
+ try {
367
+ const rawHtml = marked.parse(readFileSync(path, "utf8"), { async: false }) as string;
368
+ return sanitizeHtml(rawHtml, {
369
+ allowedTags: [
370
+ "h1", "h2", "h3", "h4",
371
+ "p", "br", "hr",
372
+ "ul", "ol", "li",
373
+ "strong", "em", "b", "i", "u", "s", "del",
374
+ "a", "code", "pre", "blockquote", "img",
375
+ ],
376
+ allowedAttributes: {
377
+ a: ["href", "title", "rel", "target"],
378
+ img: ["src", "alt", "title", "width", "height", "loading"],
379
+ },
380
+ allowedSchemes: ["http", "https", "mailto", "tel"],
381
+ allowedSchemesByTag: {
382
+ a: ["http", "https", "mailto", "tel"],
383
+ img: ["http", "https"],
384
+ },
385
+ transformTags: {
386
+ a: sanitizeHtml.simpleTransform("a", {
387
+ rel: "noopener noreferrer",
388
+ target: "_blank",
389
+ }, true),
390
+ },
391
+ disallowedTagsMode: "discard",
392
+ });
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
@@ -21,8 +21,8 @@ const description = `That page doesn't exist on ${siteConfig.name}.`;
21
21
  site={siteConfig}
22
22
  noindex
23
23
  >
24
- <section class="grove-section">
25
- <div class="grove-container mx-auto px-5 py-20 sm:px-7 sm:py-28">
24
+ <section class="py-12 sm:py-16">
25
+ <div class="mx-auto w-full max-w-container px-5 py-20 sm:px-7 sm:py-28">
26
26
  <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
27
27
  404
28
28
  </span>
@@ -50,14 +50,14 @@ const description = `That page doesn't exist on ${siteConfig.name}.`;
50
50
  class="w-full rounded-md border border-ink-200 bg-white py-2.5 px-3 text-sm placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
51
51
  />
52
52
  </label>
53
- <button type="submit" class="grove-btn grove-btn-primary">
53
+ <button type="submit" 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">
54
54
  Search
55
55
  </button>
56
56
  </form>
57
57
 
58
58
  <div class="mt-6 flex flex-wrap gap-3">
59
- <a href="/" class="grove-btn grove-btn-primary">Back to home</a>
60
- <a href={`/${slug}`} class="grove-btn grove-btn-outline">Browse {slug}</a>
59
+ <a href="/" 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">Back to home</a>
60
+ <a href={`/${slug}`} 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">Browse {slug}</a>
61
61
  </div>
62
62
  </div>
63
63
  </section>
@@ -64,7 +64,7 @@ if (!record) {
64
64
  }
65
65
 
66
66
  const isProject = record.kind === "project";
67
- const proj: ProjectRecord | null = project;
67
+ const proj: ProjectRecord | null = project ?? null;
68
68
 
69
69
  const name =
70
70
  record.kind === "resource" ? (record as { title: string }).title : (record as { name: string }).name;
@@ -101,18 +101,14 @@ const tags = record.tags ?? [];
101
101
  const healthStatus = proj?.health?.status;
102
102
  const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
103
103
 
104
- // ── Badge class lookup ────────────────────────────────────────────
105
- // The previous code interpolated `LabelId` straight into the class
106
- // string (`grove-badge-{l}`), which Astro renders as a literal class
107
- // name — every badge became `grove-badge-{l}` regardless of value.
108
- // Map each known id to its stable CSS class in the package
109
- // stylesheet. Unknown ids (e.g. legacy "featured") fall back to the
110
- // neutral `.grove-badge` look instead of producing broken classes.
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.
111
107
  const labelClassMap: Record<LabelId, string> = {
112
- new: "grove-badge-new",
113
- hot: "grove-badge-hot",
114
- mature: "grove-badge-mature",
115
- featured: "grove-badge",
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",
116
112
  };
117
113
 
118
114
  // ── Optional content markdown (record.content is a path under
@@ -224,18 +220,20 @@ if (isProject && proj) {
224
220
  keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
225
221
  isAccessibleForFree: true,
226
222
  };
227
- } else {
223
+ } else if (entity) {
228
224
  jsonLd = {
229
225
  "@context": "https://schema.org",
230
- "@type": record.type === "person" ? "Person" : "Organization",
226
+ "@type": entity.type === "person" ? "Person" : "Organization",
231
227
  name,
232
228
  headline: name,
233
- description: record.description || undefined,
229
+ description: entity.description || undefined,
234
230
  url: homepageUrl || undefined,
235
- foundingDate: record.founded || undefined,
236
- location: record.location || undefined,
237
- keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
231
+ foundingDate: entity.founded || undefined,
232
+ location: entity.location || undefined,
233
+ keywords: (entity.tags ?? []).length > 0 ? (entity.tags ?? []).join(", ") : undefined,
238
234
  };
235
+ } else {
236
+ jsonLd = {};
239
237
  }
240
238
  ---
241
239
 
@@ -248,8 +246,8 @@ if (isProject && proj) {
248
246
  submitHref="/submit"
249
247
  submitLabel={`Submit ${name}`}
250
248
  >
251
- <article class="grove-section">
252
- <div class="grove-container-wide mx-auto px-5 sm:px-7">
249
+ <article class="py-12 sm:py-16">
250
+ <div class="mx-auto w-full max-w-wide px-5 sm:px-7">
253
251
  <nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
254
252
  <a href={`/${slug}`} class="hover:text-ink-900 dark:hover:text-ink-100 capitalize">
255
253
  {slug}
@@ -259,7 +257,7 @@ if (isProject && proj) {
259
257
  </nav>
260
258
 
261
259
  {/* ── AppCard-style header ──────────────────────────────── */}
262
- <header class="grove-card flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
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">
263
261
  <div class="flex-shrink-0">
264
262
  {avatarSrc ? (
265
263
  <img
@@ -292,7 +290,7 @@ if (isProject && proj) {
292
290
  </span>
293
291
  )}
294
292
  {curationLabels.map((l) => (
295
- <span class={`grove-badge ${labelClassMap[l as LabelId] ?? ""}`} title={`Curated: ${labelDisplay(l) ?? 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}`}>
296
294
  {labelDisplay(l) ?? prettySlug(l)}
297
295
  </span>
298
296
  ))}
@@ -308,7 +306,7 @@ if (isProject && proj) {
308
306
  </span>
309
307
  )}
310
308
  {healthLabel && (
311
- <span class="grove-badge grove-badge-stale">{healthLabel}</span>
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>
312
310
  )}
313
311
  </div>
314
312
 
@@ -350,7 +348,7 @@ if (isProject && proj) {
350
348
  href={repoUrl}
351
349
  target="_blank"
352
350
  rel="noopener noreferrer"
353
- class="grove-btn grove-btn-primary"
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"
354
352
  >
355
353
  View on GitHub →
356
354
  </a>
@@ -360,7 +358,7 @@ if (isProject && proj) {
360
358
  href={homepageUrl}
361
359
  target="_blank"
362
360
  rel="noopener noreferrer"
363
- class="grove-btn grove-btn-outline"
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"
364
362
  >
365
363
  Homepage
366
364
  </a>
@@ -371,7 +369,7 @@ if (isProject && proj) {
371
369
 
372
370
  {/* ── Score bars ───────────────────────────────────────── */}
373
371
  {proj && (
374
- <section class="mt-8 grove-card p-6" aria-labelledby="scores-heading">
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">
375
373
  <h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
376
374
  Scores
377
375
  </h2>
@@ -396,7 +394,7 @@ if (isProject && proj) {
396
394
 
397
395
  {/* ── GitHub signals dl ────────────────────────────────── */}
398
396
  {isProject && github && (
399
- <section class="mt-8 grove-card p-6" aria-labelledby="signals-heading">
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">
400
398
  <h2 id="signals-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
401
399
  GitHub signals
402
400
  </h2>
@@ -461,8 +459,8 @@ if (isProject && proj) {
461
459
  {/* ── Decision signals (3-col) ──────────────────────────── */}
462
460
  {isProject && github && (
463
461
  <section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3" aria-label="Decision signals">
464
- {/* Repository health 3-col card */}
465
- <div class="grove-card p-5">
462
+ {/* Repository health card */}
463
+ <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
466
464
  <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
467
465
  Repository health
468
466
  </h3>
@@ -501,7 +499,7 @@ if (isProject && proj) {
501
499
  </div>
502
500
 
503
501
  {/* Activity bar chart */}
504
- <div class="grove-card p-5">
502
+ <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
505
503
  <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
506
504
  Activity
507
505
  </h3>
@@ -537,7 +535,7 @@ if (isProject && proj) {
537
535
  </div>
538
536
 
539
537
  {/* Contribution readiness chips */}
540
- <div class="grove-card p-5">
538
+ <div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
541
539
  <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
542
540
  Contribution readiness
543
541
  </h3>
@@ -563,7 +561,7 @@ if (isProject && proj) {
563
561
 
564
562
  {/* ── Language mix ─────────────────────────────────────── */}
565
563
  {isProject && languages.length > 0 && (
566
- <section class="mt-4 grove-card p-5">
564
+ <section class="mt-4 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
567
565
  <h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
568
566
  Language mix
569
567
  </h3>
@@ -597,7 +595,7 @@ if (isProject && proj) {
597
595
  Stack
598
596
  </span>
599
597
  {stacks.map((s) => (
600
- <span class="grove-badge inline-flex items-center gap-1.5">
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">
601
599
  <Icon name={s} category="stack" size={14} />
602
600
  {s}
603
601
  </span>
@@ -610,7 +608,7 @@ if (isProject && proj) {
610
608
  Platform
611
609
  </span>
612
610
  {platforms.map((p) => (
613
- <span class="grove-badge inline-flex items-center gap-1.5">
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">
614
612
  <Icon name={p} category="platform" size={14} />
615
613
  {p}
616
614
  </span>
@@ -632,7 +630,7 @@ if (isProject && proj) {
632
630
  <li>
633
631
  <a
634
632
  href={`/${slug}?q=${encodeURIComponent(t)}`}
635
- class="grove-badge inline-flex items-center gap-1.5"
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"
636
634
  >
637
635
  {t}
638
636
  </a>
@@ -650,12 +648,12 @@ if (isProject && proj) {
650
648
  </h2>
651
649
  <ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
652
650
  {distribution.map((c) => (
653
- <li class="grove-card flex flex-wrap items-baseline justify-between gap-2 p-3">
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">
654
652
  <div class="flex flex-col">
655
653
  <span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
656
654
  {c.label ?? c.type}
657
655
  {c.verified && (
658
- <span class="ml-2 grove-badge grove-badge-new">Verified</span>
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>
659
657
  )}
660
658
  </span>
661
659
  {c.notes && (
@@ -666,7 +664,7 @@ if (isProject && proj) {
666
664
  href={c.url}
667
665
  target="_blank"
668
666
  rel="noopener noreferrer"
669
- class="grove-btn grove-btn-outline"
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"
670
668
  >
671
669
  {c.platform ?? c.type} →
672
670
  </a>