@grove-dev/astro 0.2.2 → 0.2.5

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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +4 -4
  3. package/src/components/ItemCard.astro +35 -18
  4. package/src/components/{AppSection.astro → RecordSection.astro} +11 -6
  5. package/templates/default/src/data/records.ts +61 -29
  6. package/templates/default/src/pages/index.astro +23 -18
  7. package/templates/default/.astro/content.d.ts +0 -155
  8. package/templates/default/.astro/settings.json +0 -5
  9. package/templates/default/.astro/types.d.ts +0 -2
  10. package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +0 -1
  11. package/templates/default/dist/about/index.html +0 -165
  12. package/templates/default/dist/icons/platforms/android.svg +0 -1
  13. package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
  14. package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
  15. package/templates/default/dist/icons/platforms/chrome.svg +0 -1
  16. package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
  17. package/templates/default/dist/icons/platforms/desktop.svg +0 -4
  18. package/templates/default/dist/icons/platforms/embedded.svg +0 -5
  19. package/templates/default/dist/icons/platforms/ios.svg +0 -1
  20. package/templates/default/dist/icons/platforms/linux.svg +0 -1
  21. package/templates/default/dist/icons/platforms/macos.svg +0 -1
  22. package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
  23. package/templates/default/dist/icons/platforms/web.svg +0 -5
  24. package/templates/default/dist/icons/platforms/windows.svg +0 -6
  25. package/templates/default/dist/icons/stacks/android.svg +0 -1
  26. package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
  27. package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
  28. package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
  29. package/templates/default/dist/icons/stacks/dart.svg +0 -1
  30. package/templates/default/dist/icons/stacks/firebase.svg +0 -1
  31. package/templates/default/dist/icons/stacks/flutter.svg +0 -1
  32. package/templates/default/dist/icons/stacks/graphql.svg +0 -1
  33. package/templates/default/dist/icons/stacks/ionic.svg +0 -4
  34. package/templates/default/dist/icons/stacks/java.svg +0 -1
  35. package/templates/default/dist/icons/stacks/javascript.svg +0 -1
  36. package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
  37. package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
  38. package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
  39. package/templates/default/dist/icons/stacks/python.svg +0 -1
  40. package/templates/default/dist/icons/stacks/react-native.svg +0 -1
  41. package/templates/default/dist/icons/stacks/react.svg +0 -1
  42. package/templates/default/dist/icons/stacks/rust.svg +0 -1
  43. package/templates/default/dist/icons/stacks/solidity.svg +0 -1
  44. package/templates/default/dist/icons/stacks/swift.svg +0 -1
  45. package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
  46. package/templates/default/dist/icons/stacks/typescript.svg +0 -1
  47. package/templates/default/dist/index.html +0 -55
  48. package/templates/default/dist/llms-full.txt +0 -14
  49. package/templates/default/dist/llms.txt +0 -11
  50. package/templates/default/dist/og-image.svg +0 -48
  51. package/templates/default/dist/projects/index.html +0 -61
  52. package/templates/default/dist/robots.txt +0 -53
  53. package/templates/default/dist/sitemap.xml +0 -21
  54. package/templates/default/dist/submit/index.html +0 -69
  55. package/templates/default/node_modules/.bin/astro +0 -21
  56. package/templates/default/node_modules/.bin/astro-check +0 -21
  57. package/templates/default/node_modules/.bin/grove +0 -21
  58. package/templates/default/node_modules/.bin/jiti +0 -21
  59. package/templates/default/node_modules/.bin/prettier +0 -21
  60. package/templates/default/node_modules/.bin/rollup +0 -21
  61. package/templates/default/node_modules/.bin/tsc +0 -21
  62. package/templates/default/node_modules/.bin/tsserver +0 -21
  63. package/templates/default/node_modules/.bin/tsx +0 -21
  64. package/templates/default/node_modules/.bin/vite +0 -21
  65. package/templates/default/node_modules/.bin/yaml +0 -21
  66. package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
  67. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
  68. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
  69. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
  70. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
  71. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
  72. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
  73. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
  74. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
  75. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
  76. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
  77. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
  78. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
  79. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
  80. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
  81. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
  82. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
  83. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
  84. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
  85. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
  86. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
  87. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
  88. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
  89. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
  90. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
  91. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
  92. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
  93. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
  94. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
  95. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
  96. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
  97. package/templates/default/node_modules/.vite/deps/package.json +0 -3
  98. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
  99. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
  100. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
  101. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
  102. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
  103. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
  104. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
  105. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +0 -7
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Turtuvshin Byambaa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grove-dev/astro",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Astro framework adapter for Grove. Components, layouts, design tokens, and a default template.",
6
6
  "license": "MIT",
@@ -43,9 +43,9 @@
43
43
  "./layouts/*": "./src/layouts/*.astro"
44
44
  },
45
45
  "dependencies": {
46
- "astro": "^6.4.4",
47
- "@grove-dev/core": "0.2.2",
48
- "@grove-dev/ui": "0.2.2"
46
+ "astro": "^6.4.6",
47
+ "@grove-dev/core": "0.2.5",
48
+ "@grove-dev/ui": "0.2.5"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "astro": "^6.0.0"
@@ -22,17 +22,24 @@
22
22
  * `curation`, `health`); for other kinds we degrade gracefully to
23
23
  * the minimal view.
24
24
  *
25
- * The component is index-payload-friendly: every field it reads is
26
- * present in the `toIndexRecord` projection (see
27
- * `@grove-dev/core/schema.ts`).
25
+ * Index-payload-friendly: when the consumer passes an
26
+ * `IndexProjectRecord` (the slim list-page shape), the component
27
+ * reads the projected fields directly — `github.stars` /
28
+ * `github.pushedAt` / `github.license` — rather than the raw
29
+ * `github.repository` block.
28
30
  */
29
- import type { Resource, ProjectRecord, HealthStatus } from "@grove-dev/core";
31
+ import type {
32
+ Resource,
33
+ ProjectRecord,
34
+ HealthStatus,
35
+ IndexProjectRecord,
36
+ } from "@grove-dev/core";
30
37
  import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../lib/repo";
31
38
  import { formatRelative, formatStars } from "../lib/format";
32
39
  import { statusDisplay, prettySlug, labelDisplay } from "../lib/display";
33
40
 
34
41
  interface Props {
35
- record: Resource;
42
+ record: Resource | IndexProjectRecord;
36
43
  /** Detail page href. */
37
44
  href: string;
38
45
  /** When true, show owner/repo below the title. Default true. */
@@ -57,9 +64,13 @@ const {
57
64
  class: className = "",
58
65
  } = Astro.props;
59
66
 
60
- // Project-specific extraction
61
- const isProject = record.kind === "project";
62
- const project = isProject ? (record as ProjectRecord) : null;
67
+ // Project-specific extraction. The `isIndex` branch reads the slim
68
+ // fields from `toIndexRecord`; the full branch reads the raw
69
+ // `github.repository` block for sites that want max fidelity.
70
+ const isIndex = "github" in record && record.github !== undefined
71
+ && (record as IndexProjectRecord).github !== undefined
72
+ && (record.github as { fullName?: string })?.fullName !== undefined;
73
+ const project = record.kind === "project" ? (record as ProjectRecord | IndexProjectRecord) : null;
63
74
 
64
75
  const name =
65
76
  record.kind === "resource"
@@ -67,17 +78,20 @@ const name =
67
78
  : (record as { name: string }).name;
68
79
  const description = record.description ?? "";
69
80
  const tags = (record.tags ?? []).slice(0, 4);
70
- const repoUrl = project?.repoUrl;
71
- const logoUrl = project?.logoUrl;
72
- const stacks = project?.stacks ?? (project?.stack ? [project.stack] : []);
73
- const platforms = project?.platforms ?? [];
81
+ const repoUrl = (project as ProjectRecord | IndexProjectRecord | null)?.repoUrl;
82
+ const logoUrl = (project as ProjectRecord | IndexProjectRecord | null)?.logoUrl;
83
+ const stacks = (project?.stacks ?? (project?.stack ? [project.stack] : [])) as string[];
84
+ const platforms = (project?.platforms ?? []) as string[];
74
85
  const healthStatus: HealthStatus | undefined = project?.health?.status;
75
86
  const labels = record.curation?.labels ?? [];
76
- const github = project?.github?.repository;
77
- const stars = github?.stargazers_count;
78
- const licenseSpdx = github?.license?.spdx_id;
79
- const pushedAt = github?.pushed_at;
80
- const githubUrl = repoUrl ?? (record.links as { github?: string } | undefined)?.github;
87
+ // Slim (IndexProjectRecord.github) full (ProjectRecord.github.repository).
88
+ const indexGh = isIndex ? (project as IndexProjectRecord).github : null;
89
+ const fullGh = !isIndex ? (project as ProjectRecord | null)?.github?.repository : null;
90
+ const stars = indexGh?.stars ?? fullGh?.stargazers_count;
91
+ const licenseSpdx = indexGh?.license ?? fullGh?.license?.spdx_id ?? null;
92
+ const pushedAt = indexGh?.pushedAt ?? fullGh?.pushed_at;
93
+ const githubUrl =
94
+ repoUrl ?? (record.links as { github?: string } | undefined)?.github;
81
95
 
82
96
  // Logo fallback chain
83
97
  const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
@@ -102,7 +116,10 @@ const platformChips = withOverflow(platforms);
102
116
  // Curated mark
103
117
  const curatedLabel = labels[0] ?? null;
104
118
 
105
- // Health class hook (for badge variant)
119
+ // Health class hook (for badge variant). `project` is the union of
120
+ // ProjectRecord | IndexProjectRecord, and both share a `health` field
121
+ // with the same shape, so a single optional-chain read covers both
122
+ // shapes — no isIndex branch is needed for the status itself.
106
123
  function healthBadgeClass(s: HealthStatus | undefined): string {
107
124
  switch (s) {
108
125
  case "mature":
@@ -1,17 +1,22 @@
1
1
  ---
2
2
  /**
3
- * AppSection — a titled "lens" of records on the home page.
3
+ * RecordSection — a titled "lens" of records on the home page.
4
4
  *
5
5
  * Renders a SectionHeader + a 3-column grid of `ItemCard`s, with an
6
6
  * optional "View all" link in the header. Generic: passes the
7
7
  * record list + per-record href through to `ItemCard`.
8
8
  *
9
9
  * The component is the workhorse of the home page — featured, hot,
10
- * new, mature, "by category", "by stack" — all of them are just an
11
- * `AppSection` with a different `id` + `title` + sliced list.
10
+ * new, mature, "by category", "by stack" — all of them are just a
11
+ * `RecordSection` with a different `id` + `title` + sliced list.
12
+ *
13
+ * V1: items are `IndexProjectRecord`s (the slim list-page shape).
14
+ * Other blueprints (`resource-hub`, `ecosystem-map`) can pass their
15
+ * own `IndexResourceRecord` / `IndexEntityRecord` arrays; the
16
+ * component does not branch on the kind.
12
17
  */
13
18
 
14
- import type { Resource } from "@grove-dev/core";
19
+ import type { IndexProjectRecord } from "@grove-dev/core";
15
20
  import ItemCard from "./ItemCard.astro";
16
21
  import SectionHeader from "../layouts/SectionHeader.astro";
17
22
 
@@ -20,9 +25,9 @@ interface Props {
20
25
  id: string;
21
26
  title: string;
22
27
  description?: string;
23
- items: Resource[];
28
+ items: IndexProjectRecord[];
24
29
  /** Detail page href builder. */
25
- hrefFor: (record: Resource) => string;
30
+ hrefFor: (record: IndexProjectRecord) => string;
26
31
  /** Optional "View all" link rendered in the header. */
27
32
  viewAllHref?: string;
28
33
  viewAllLabel?: string;
@@ -10,16 +10,14 @@
10
10
  * `whyListed`, `caveats`, full `github.repository`, ...).
11
11
  * Use this for the detail page.
12
12
  * - `records.index.json` — slim projection, visible-only. Use
13
- * this for the list page and any home-page sectioning.
13
+ * this for the list page and any home-page sectioning. Shape
14
+ * is the `IndexRecord` discriminated union from `@grove-dev/core`.
14
15
  * - `records.json` — alias of `records.full.json`.
15
16
  *
16
17
  * The YML files are the human-edited source; this module is a
17
18
  * typed re-export so pages can import the records without parsing
18
- * JSON inline. The shape is the V1 discriminated union from
19
- * `@grove-dev/core` records have a `kind` field that selects
20
- * `ProjectRecord`, `ResourceRecord`, or `EntityRecord`. Pages cast
21
- * to the kind they expect based on the blueprint in
22
- * `grove.config.ts`.
19
+ * JSON inline. Pages cast to the kind they expect based on the
20
+ * blueprint in `grove.config.ts`.
23
21
  *
24
22
  * When the JSON files are missing (e.g. before `grove generate`
25
23
  * runs, or in a fresh scaffold with no records) this module falls
@@ -29,9 +27,18 @@
29
27
  import { existsSync, readFileSync } from "node:fs";
30
28
  import { fileURLToPath } from "node:url";
31
29
  import { dirname, resolve } from "node:path";
32
- import type { ProjectRecord, ResourceRecord, EntityRecord, Resource } from "@grove-dev/core";
33
-
34
- interface RecordsPayload {
30
+ import type {
31
+ ProjectRecord,
32
+ ResourceRecord,
33
+ EntityRecord,
34
+ Resource,
35
+ IndexRecord,
36
+ IndexProjectRecord,
37
+ IndexResourceRecord,
38
+ IndexEntityRecord,
39
+ } from "@grove-dev/core";
40
+
41
+ interface FullPayload {
35
42
  schemaVersion?: number;
36
43
  blueprint?: string;
37
44
  generatedAt?: string;
@@ -40,7 +47,15 @@ interface RecordsPayload {
40
47
  records?: Resource[];
41
48
  }
42
49
 
43
- function loadGenerated(filename: string): Resource[] {
50
+ interface IndexPayload {
51
+ schemaVersion?: number;
52
+ blueprint?: string;
53
+ generatedAt?: string;
54
+ totalRecords?: number;
55
+ records?: IndexRecord[];
56
+ }
57
+
58
+ function loadGenerated<T>(filename: string, parser: (raw: string) => T): T {
44
59
  // The Astro build resolves JSON imports from `src/data/*.ts`, but
45
60
  // we want to be defensive about the order of operations: a fresh
46
61
  // scaffold runs `astro build` without first running
@@ -55,17 +70,38 @@ function loadGenerated(filename: string): Resource[] {
55
70
  resolve(process.cwd(), "data", "generated", filename),
56
71
  ];
57
72
  const path = candidates.find((p) => existsSync(p));
58
- if (!path) return [];
59
- const raw = readFileSync(path, "utf8");
60
- const parsed = JSON.parse(raw) as RecordsPayload;
61
- return (parsed.records ?? []) as Resource[];
73
+ if (!path) return parser("");
74
+ return parser(readFileSync(path, "utf8"));
62
75
  } catch {
63
- return [];
76
+ return parser("");
64
77
  }
65
78
  }
66
79
 
67
- const fullRecordsRaw = loadGenerated("records.full.json");
68
- const indexRecordsRaw = loadGenerated("records.index.json");
80
+ const fullRecordsRaw: Resource[] = loadGenerated(
81
+ "records.full.json",
82
+ (raw) => {
83
+ if (!raw) return [];
84
+ try {
85
+ const parsed = JSON.parse(raw) as FullPayload;
86
+ return (parsed.records ?? []) as Resource[];
87
+ } catch {
88
+ return [];
89
+ }
90
+ },
91
+ );
92
+
93
+ const indexRecordsRaw: IndexRecord[] = loadGenerated(
94
+ "records.index.json",
95
+ (raw) => {
96
+ if (!raw) return [];
97
+ try {
98
+ const parsed = JSON.parse(raw) as IndexPayload;
99
+ return (parsed.records ?? []) as IndexRecord[];
100
+ } catch {
101
+ return [];
102
+ }
103
+ },
104
+ );
69
105
 
70
106
  /**
71
107
  * Full records (all visibility). Use this for the detail page,
@@ -79,22 +115,22 @@ export const fullRecords: Resource[] = fullRecordsRaw;
79
115
  * page and any home-page sectioning, where you only need the
80
116
  * slim search-index fields.
81
117
  */
82
- export const records: Resource[] =
83
- indexRecordsRaw.length > 0 ? indexRecordsRaw : fullRecordsRaw;
118
+ export const records: IndexRecord[] =
119
+ indexRecordsRaw.length > 0 ? indexRecordsRaw : [];
84
120
 
85
- /** Project-kind records only. */
121
+ /** Project-kind records only — slim shape, ready for list pages. */
86
122
  export const projects = records.filter(
87
- (r): r is ProjectRecord => r.kind === "project",
123
+ (r): r is IndexProjectRecord => r.kind === "project",
88
124
  );
89
125
 
90
- /** Resource-kind records. */
126
+ /** Resource-kind records — slim shape. */
91
127
  export const resources = records.filter(
92
- (r): r is ResourceRecord => r.kind === "resource",
128
+ (r): r is IndexResourceRecord => r.kind === "resource",
93
129
  );
94
130
 
95
- /** Entity-kind records. */
131
+ /** Entity-kind records — slim shape. */
96
132
  export const entities = records.filter(
97
- (r): r is EntityRecord => r.kind === "entity",
133
+ (r): r is IndexEntityRecord => r.kind === "entity",
98
134
  );
99
135
 
100
136
  const bySlug = new Map(fullRecords.map((r) => [r.slug, r]));
@@ -106,10 +142,6 @@ export function recordBySlug(slug: string): Resource | undefined {
106
142
  /**
107
143
  * Generic slug lookup. The spec name is `findRecord`; this is an
108
144
  * alias of `recordBySlug` so consumers can use either spelling.
109
- * The two-name pattern matches the rest of the module (we keep
110
- * `recordBySlug` for the typed `projectBySlug`/`resourceBySlug`/
111
- * `entityBySlug` family, and `findRecord` as the unified entry
112
- * point).
113
145
  */
114
146
  export function findRecord(slug: string): Resource | undefined {
115
147
  return bySlug.get(slug);
@@ -5,9 +5,9 @@
5
5
  * Sections (in order):
6
6
  * 1. Hero — brand + search + stats row
7
7
  * 2. WhyThisExists — 3 short value-prop points
8
- * 3. AppSection "Trending" — hot lens (stars >= 1000 OR pushed <= 30d)
9
- * 4. AppSection "Recently added"— new lens (pushed <= 7d OR curated "new")
10
- * 5. AppSection "Established" — mature lens (tier=curated, pushed > 365d)
8
+ * 3. RecordSection "Trending" — hot lens (stars >= 1000 OR pushed <= 30d)
9
+ * 4. RecordSection "Recently added" — new lens (pushed <= 7d OR curated "new")
10
+ * 5. RecordSection "Established" — mature lens (tier=curated, pushed > 365d)
11
11
  * 6. StackGrid — browse by stack
12
12
  * 7. CategoryGrid — browse by category
13
13
  * 8. ContributorsGrid — top contributors (when available)
@@ -15,13 +15,18 @@
15
15
  *
16
16
  * Every section is defensive: with 0 records (fresh scaffold) it
17
17
  * renders a graceful empty state instead of throwing.
18
+ *
19
+ * V1 source-of-truth types are from `@grove-dev/core`. The slim
20
+ * `IndexProjectRecord` shape is what the home page consumes (no
21
+ * `as ProjectRecord` casts); the full `Resource` only flows into
22
+ * the detail page.
18
23
  */
19
24
  import siteConfig from "../../data/generated/site-config.json";
20
25
  import { records, fullRecords, projects, indexSlug, itemLabel } from "../data/records";
21
26
  import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
22
27
  import Hero from "@grove-dev/astro/components/Hero.astro";
23
28
  import WhyThisExists from "@grove-dev/astro/components/WhyThisExists.astro";
24
- import AppSection from "@grove-dev/astro/components/AppSection.astro";
29
+ import RecordSection from "@grove-dev/astro/components/RecordSection.astro";
25
30
  import StackGrid from "@grove-dev/astro/components/StackGrid.astro";
26
31
  import CategoryGrid from "@grove-dev/astro/components/CategoryGrid.astro";
27
32
  import ContributorsGrid from "@grove-dev/astro/components/ContributorsGrid.astro";
@@ -30,7 +35,7 @@ import { countByCategory, countByStack } from "@grove-dev/astro";
30
35
  import { existsSync, readFileSync } from "node:fs";
31
36
  import { fileURLToPath } from "node:url";
32
37
  import { dirname, resolve } from "node:path";
33
- import type { ProjectRecord } from "@grove-dev/core";
38
+ import type { IndexProjectRecord } from "@grove-dev/core";
34
39
 
35
40
  const slug = indexSlug();
36
41
  const projectKind = projects;
@@ -45,14 +50,14 @@ const totalStacks = new Set(
45
50
  const totalContributors = new Set(
46
51
  projectKind
47
52
  .map((r) => {
48
- const url = r.repoUrl ?? (r.links as { github?: string } | undefined)?.github ?? "";
53
+ const url = r.repoUrl ?? r.links?.github ?? "";
49
54
  const m = /github\.com\/([^/]+)\//.exec(url);
50
55
  return m?.[1];
51
56
  })
52
57
  .filter(Boolean),
53
58
  ).size;
54
59
  const totalStars = projectKind.reduce(
55
- (acc, r) => acc + ((r as ProjectRecord).github?.stars ?? 0),
60
+ (acc, r) => acc + (r.github?.stars ?? 0),
56
61
  0,
57
62
  );
58
63
 
@@ -66,9 +71,9 @@ function daysSince(iso: string | null | undefined): number {
66
71
  }
67
72
 
68
73
  const hot = projectKind
69
- .filter((r) => {
70
- const stars = (r as ProjectRecord).github?.stars ?? 0;
71
- const pushedAt = (r as ProjectRecord).github?.pushedAt ?? null;
74
+ .filter((r: IndexProjectRecord) => {
75
+ const stars = r.github?.stars ?? 0;
76
+ const pushedAt = r.github?.pushedAt ?? null;
72
77
  const pushed = daysSince(pushedAt);
73
78
  const labelledHot = r.curation?.labels?.includes("hot") ?? false;
74
79
  return labelledHot || stars >= 1000 || pushed <= 30;
@@ -76,8 +81,8 @@ const hot = projectKind
76
81
  .slice(0, 6);
77
82
 
78
83
  const newItems = projectKind
79
- .filter((r) => {
80
- const pushedAt = (r as ProjectRecord).github?.pushedAt ?? null;
84
+ .filter((r: IndexProjectRecord) => {
85
+ const pushedAt = r.github?.pushedAt ?? null;
81
86
  const pushed = daysSince(pushedAt);
82
87
  const labelledNew = r.curation?.labels?.includes("new") ?? false;
83
88
  return pushed <= 7 || labelledNew;
@@ -85,9 +90,9 @@ const newItems = projectKind
85
90
  .slice(0, 6);
86
91
 
87
92
  const mature = projectKind
88
- .filter((r) => {
93
+ .filter((r: IndexProjectRecord) => {
89
94
  const tier = r.health?.tier;
90
- const pushedAt = (r as ProjectRecord).github?.pushedAt ?? null;
95
+ const pushedAt = r.github?.pushedAt ?? null;
91
96
  const pushed = daysSince(pushedAt);
92
97
  return tier === "curated" && pushed > 365;
93
98
  })
@@ -159,7 +164,7 @@ const description =
159
164
  siteConfig.description ?? `A growing community knowledge site powered by Grove.`;
160
165
 
161
166
  // ── Render-time helpers ─────────────────────────────────────────
162
- function detailHrefFor(r: (typeof records)[number]): string {
167
+ function detailHrefFor(r: IndexProjectRecord): string {
163
168
  return `/${slug}/${r.slug}`;
164
169
  }
165
170
  ---
@@ -193,7 +198,7 @@ function detailHrefFor(r: (typeof records)[number]): string {
193
198
 
194
199
  <WhyThisExists />
195
200
 
196
- <AppSection
201
+ <RecordSection
197
202
  id="hot"
198
203
  title="Trending"
199
204
  description="The projects getting attention right now — high-star repos and recently active codebases."
@@ -204,7 +209,7 @@ function detailHrefFor(r: (typeof records)[number]): string {
204
209
  emptyLabel="No trending projects yet."
205
210
  />
206
211
 
207
- <AppSection
212
+ <RecordSection
208
213
  id="new"
209
214
  title="Recently added"
210
215
  description="Fresh entries — pushed within the last week or carrying the curated 'new' label."
@@ -215,7 +220,7 @@ function detailHrefFor(r: (typeof records)[number]): string {
215
220
  emptyLabel="No new projects yet."
216
221
  />
217
222
 
218
- <AppSection
223
+ <RecordSection
219
224
  id="mature"
220
225
  title="Established"
221
226
  description="Long-running curated projects with deep history and stable architecture."
@@ -1,155 +0,0 @@
1
- declare module 'astro:content' {
2
- export interface RenderResult {
3
- Content: import('astro/runtime/server/index.js').AstroComponentFactory;
4
- headings: import('astro').MarkdownHeading[];
5
- remarkPluginFrontmatter: Record<string, any>;
6
- }
7
- interface Render {
8
- '.md': Promise<RenderResult>;
9
- }
10
-
11
- export interface RenderedContent {
12
- html: string;
13
- metadata?: {
14
- imagePaths: Array<string>;
15
- [key: string]: unknown;
16
- };
17
- }
18
-
19
- type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
20
-
21
- export type CollectionKey = keyof DataEntryMap;
22
- export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
23
-
24
- type AllValuesOf<T> = T extends any ? T[keyof T] : never;
25
-
26
- export type ReferenceDataEntry<
27
- C extends CollectionKey,
28
- E extends keyof DataEntryMap[C] = string,
29
- > = {
30
- collection: C;
31
- id: E;
32
- };
33
-
34
- export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
35
- collection: C;
36
- id: string;
37
- };
38
-
39
- export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
40
- collection: C,
41
- filter?: (entry: CollectionEntry<C>) => entry is E,
42
- ): Promise<E[]>;
43
- export function getCollection<C extends keyof DataEntryMap>(
44
- collection: C,
45
- filter?: (entry: CollectionEntry<C>) => unknown,
46
- ): Promise<CollectionEntry<C>[]>;
47
-
48
- export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
49
- collection: C,
50
- filter?: LiveLoaderCollectionFilterType<C>,
51
- ): Promise<
52
- import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
53
- >;
54
-
55
- export function getEntry<
56
- C extends keyof DataEntryMap,
57
- E extends keyof DataEntryMap[C] | (string & {}),
58
- >(
59
- entry: ReferenceDataEntry<C, E>,
60
- ): E extends keyof DataEntryMap[C]
61
- ? Promise<DataEntryMap[C][E]>
62
- : Promise<CollectionEntry<C> | undefined>;
63
- export function getEntry<
64
- C extends keyof DataEntryMap,
65
- E extends keyof DataEntryMap[C] | (string & {}),
66
- >(
67
- collection: C,
68
- id: E,
69
- ): E extends keyof DataEntryMap[C]
70
- ? string extends keyof DataEntryMap[C]
71
- ? Promise<DataEntryMap[C][E]> | undefined
72
- : Promise<DataEntryMap[C][E]>
73
- : Promise<CollectionEntry<C> | undefined>;
74
- export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
75
- collection: C,
76
- filter: string | LiveLoaderEntryFilterType<C>,
77
- ): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
78
-
79
- /** Resolve an array of entry references from the same collection */
80
- export function getEntries<C extends keyof DataEntryMap>(
81
- entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
82
- ): Promise<CollectionEntry<C>[]>;
83
-
84
- export function render<C extends keyof DataEntryMap>(
85
- entry: DataEntryMap[C][string],
86
- ): Promise<RenderResult>;
87
-
88
- export function reference<
89
- C extends
90
- | keyof DataEntryMap
91
- // Allow generic `string` to avoid excessive type errors in the config
92
- // if `dev` is not running to update as you edit.
93
- // Invalid collection names will be caught at build time.
94
- | (string & {}),
95
- >(
96
- collection: C,
97
- ): import('astro/zod').ZodPipe<
98
- import('astro/zod').ZodString,
99
- import('astro/zod').ZodTransform<
100
- C extends keyof DataEntryMap
101
- ? {
102
- collection: C;
103
- id: string;
104
- }
105
- : never,
106
- string
107
- >
108
- >;
109
-
110
- type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
111
- type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
112
- ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
113
- >;
114
- type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
115
- type InferLoaderSchema<
116
- C extends keyof DataEntryMap,
117
- L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
118
- > = L extends { schema: import('astro/zod').ZodSchema }
119
- ? import('astro/zod').infer<L['schema']>
120
- : any;
121
-
122
- type DataEntryMap = {
123
-
124
- };
125
-
126
- type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
127
- infer TData,
128
- infer TEntryFilter,
129
- infer TCollectionFilter,
130
- infer TError
131
- >
132
- ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
133
- : { data: never; entryFilter: never; collectionFilter: never; error: never };
134
- type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
135
- type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
136
- type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
137
- type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
138
-
139
- type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
140
- LiveContentConfig['collections'][C]['schema'] extends undefined
141
- ? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
142
- : import('astro/zod').infer<
143
- Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
144
- >;
145
- type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
146
- ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
147
- type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
148
- ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
149
- type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
150
- LiveContentConfig['collections'][C]['loader']
151
- >;
152
-
153
- export type ContentConfig = never;
154
- export type LiveContentConfig = never;
155
- }
@@ -1,5 +0,0 @@
1
- {
2
- "_variables": {
3
- "lastUpdateCheck": 1781103180236
4
- }
5
- }
@@ -1,2 +0,0 @@
1
- /// <reference types="astro/client" />
2
- /// <reference path="content.d.ts" />
@@ -1 +0,0 @@
1
- @theme{ --color-ink-50: #fafafa; --color-ink-100: #f4f4f5; --color-ink-200: #e4e4e7; --color-ink-300: #d4d4d8; --color-ink-400: #a1a1aa; --color-ink-500: #71717a; --color-ink-600: #52525b; --color-ink-700: #3f3f46; --color-ink-800: #27272a; --color-ink-900: #18181b; --color-ink-950: #09090b; --color-accent: #1f6feb; --color-accent-muted: #388bfd1a; --color-emerald-50: #ecfdf5; --color-emerald-300: #6ee7b7; --color-emerald-400: #34d399; --color-emerald-700: #047857; --color-emerald-800: #065f46; --color-emerald-950: #022c22; --color-orange-50: #fff7ed; --color-orange-300: #fdba74; --color-orange-400: #fb923c; --color-orange-700: #c2410c; --color-orange-800: #9a3412; --color-orange-950: #431407; --color-blue-50: #eff6ff; --color-blue-300: #93c5fd; --color-blue-400: #60a5fa; --color-blue-700: #1d4ed8; --color-blue-800: #1e40af; --color-blue-950: #172554; --color-amber-50: #fffbeb; --color-amber-300: #fcd34d; --color-amber-400: #fbbf24; --color-amber-700: #b45309; --color-amber-800: #92400e; --color-amber-950: #451a03; --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --text-2xs: .6875rem; --text-2xs--line-height: 1rem; --container-container: 1140px; --container-wide: 1400px; --container-narrow: 720px; --radius: .375rem; --radius-lg: .5rem; --radius-xl: .75rem; --animate-fade-in-up: fade-in-up .6s ease-out both; --animate-fade-in: fade-in .8s ease-out both; --animate-gradient-shift: gradient-shift 8s ease-in-out infinite; --animate-subtle-bob: subtle-bob 3s ease-in-out infinite; }@keyframes fade-in-up{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes gradient-shift{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}@keyframes subtle-bob{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}@layer base{:root{color-scheme:light dark}html{-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;background:#fff;color:var(--color-ink-900);font-family:var(--font-sans);font-size:15px;line-height:1.55}body.dark{background:var(--color-ink-950);color:var(--color-ink-100)}a{color:inherit;text-decoration:none}::selection{background:var(--color-ink-200);color:var(--color-ink-900)}.dark ::selection{background:var(--color-ink-800);color:var(--color-ink-100)}*{scrollbar-width:thin;scrollbar-color:var(--color-ink-300) transparent}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-thumb{background:var(--color-ink-200);border-radius:4px}.dark *::-webkit-scrollbar-thumb{background:var(--color-ink-800)}input,select,textarea,button{font:inherit}}@layer components{.grove-section{padding-top:3rem;padding-bottom:3rem}@media(min-width:640px){.grove-section{padding-top:4rem;padding-bottom:4rem}}.grove-section-tight{padding-top:2.5rem;padding-bottom:2.5rem}@media(min-width:640px){.grove-section-tight{padding-top:3rem;padding-bottom:3rem}}.grove-card{border:1px solid var(--color-ink-200);background:#fff;border-radius:var(--radius-lg);transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.dark .grove-card{border-color:var(--color-ink-800);background:var(--color-ink-950)}.grove-card-hover:hover{border-color:var(--color-ink-300);background:#f4f4f599;box-shadow:0 1px 2px #0000000a,0 4px 12px -2px #0000000f}.dark .grove-card-hover:hover{border-color:var(--color-ink-700);background:#18181b66;box-shadow:0 1px 2px #0006,0 4px 12px -2px #0000004d}.grove-badge{display:inline-flex;align-items:center;gap:.25rem;border-radius:.25rem;border:1px solid var(--color-ink-200);background:var(--color-ink-50);padding:.125rem .375rem;font-size:var(--text-2xs);line-height:var(--text-2xs--line-height);font-weight:500;color:var(--color-ink-700)}.dark .grove-badge{border-color:var(--color-ink-800);background:#18181b99;color:var(--color-ink-300)}.grove-badge-accent{border-color:var(--color-ink-900);background:var(--color-ink-900);color:#fff}.dark .grove-badge-accent{border-color:var(--color-ink-100);background:var(--color-ink-100);color:var(--color-ink-900)}.grove-badge-new{border-color:#6ee7b799;background:var(--color-emerald-50);color:var(--color-emerald-800)}.dark .grove-badge-new{border-color:#065f4699;background:#022c2266;color:var(--color-emerald-300)}.grove-badge-hot{border-color:#fdba7499;background:var(--color-orange-50);color:var(--color-orange-800)}.dark .grove-badge-hot{border-color:#9a341299;background:#43140766;color:var(--color-orange-300)}.grove-badge-mature{border-color:#93c5fd99;background:var(--color-blue-50);color:var(--color-blue-800)}.dark .grove-badge-mature{border-color:#1e40af99;background:#17255466;color:var(--color-blue-300)}.grove-badge-stale{border-color:#fcd34d99;background:var(--color-amber-50);color:var(--color-amber-800)}.dark .grove-badge-stale{border-color:#92400e99;background:#451a0366;color:var(--color-amber-300)}.grove-btn{display:inline-flex;align-items:center;justify-content:center;gap:.375rem;border-radius:var(--radius);border:1px solid transparent;padding:.375rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition:color .15s ease,background-color .15s ease,border-color .15s ease;text-decoration:none;cursor:pointer}.grove-btn-ghost{color:var(--color-ink-700)}.grove-btn-ghost:hover{background:var(--color-ink-100)}.dark .grove-btn-ghost{color:var(--color-ink-300)}.dark .grove-btn-ghost:hover{background:var(--color-ink-900)}.grove-btn-outline{border-color:var(--color-ink-200);background:#fff;color:var(--color-ink-900)}.grove-btn-outline:hover{border-color:var(--color-ink-300);background:var(--color-ink-50)}.dark .grove-btn-outline{border-color:var(--color-ink-800);background:var(--color-ink-950);color:var(--color-ink-100)}.dark .grove-btn-outline:hover{border-color:var(--color-ink-700);background:#18181b66}.grove-btn-primary{border-color:var(--color-ink-900);background:var(--color-ink-900);color:#fff}.grove-btn-primary:hover{background:var(--color-ink-800)}.dark .grove-btn-primary{border-color:var(--color-ink-100);background:var(--color-ink-100);color:var(--color-ink-900)}.dark .grove-btn-primary:hover{background:#fff}}