@grove-dev/astro 0.2.2 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +4 -4
- package/src/components/ItemCard.astro +35 -18
- package/src/components/{AppSection.astro → RecordSection.astro} +11 -6
- package/templates/default/src/data/records.ts +61 -29
- package/templates/default/src/pages/index.astro +23 -18
- package/templates/default/.astro/content.d.ts +0 -155
- package/templates/default/.astro/settings.json +0 -5
- package/templates/default/.astro/types.d.ts +0 -2
- package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +0 -1
- package/templates/default/dist/about/index.html +0 -165
- package/templates/default/dist/icons/platforms/android.svg +0 -1
- package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
- package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
- package/templates/default/dist/icons/platforms/chrome.svg +0 -1
- package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
- package/templates/default/dist/icons/platforms/desktop.svg +0 -4
- package/templates/default/dist/icons/platforms/embedded.svg +0 -5
- package/templates/default/dist/icons/platforms/ios.svg +0 -1
- package/templates/default/dist/icons/platforms/linux.svg +0 -1
- package/templates/default/dist/icons/platforms/macos.svg +0 -1
- package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
- package/templates/default/dist/icons/platforms/web.svg +0 -5
- package/templates/default/dist/icons/platforms/windows.svg +0 -6
- package/templates/default/dist/icons/stacks/android.svg +0 -1
- package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
- package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
- package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
- package/templates/default/dist/icons/stacks/dart.svg +0 -1
- package/templates/default/dist/icons/stacks/firebase.svg +0 -1
- package/templates/default/dist/icons/stacks/flutter.svg +0 -1
- package/templates/default/dist/icons/stacks/graphql.svg +0 -1
- package/templates/default/dist/icons/stacks/ionic.svg +0 -4
- package/templates/default/dist/icons/stacks/java.svg +0 -1
- package/templates/default/dist/icons/stacks/javascript.svg +0 -1
- package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
- package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
- package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
- package/templates/default/dist/icons/stacks/python.svg +0 -1
- package/templates/default/dist/icons/stacks/react-native.svg +0 -1
- package/templates/default/dist/icons/stacks/react.svg +0 -1
- package/templates/default/dist/icons/stacks/rust.svg +0 -1
- package/templates/default/dist/icons/stacks/solidity.svg +0 -1
- package/templates/default/dist/icons/stacks/swift.svg +0 -1
- package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
- package/templates/default/dist/icons/stacks/typescript.svg +0 -1
- package/templates/default/dist/index.html +0 -55
- package/templates/default/dist/llms-full.txt +0 -14
- package/templates/default/dist/llms.txt +0 -11
- package/templates/default/dist/og-image.svg +0 -48
- package/templates/default/dist/projects/index.html +0 -61
- package/templates/default/dist/robots.txt +0 -53
- package/templates/default/dist/sitemap.xml +0 -21
- package/templates/default/dist/submit/index.html +0 -69
- package/templates/default/node_modules/.bin/astro +0 -21
- package/templates/default/node_modules/.bin/astro-check +0 -21
- package/templates/default/node_modules/.bin/grove +0 -21
- package/templates/default/node_modules/.bin/jiti +0 -21
- package/templates/default/node_modules/.bin/prettier +0 -21
- package/templates/default/node_modules/.bin/rollup +0 -21
- package/templates/default/node_modules/.bin/tsc +0 -21
- package/templates/default/node_modules/.bin/tsserver +0 -21
- package/templates/default/node_modules/.bin/tsx +0 -21
- package/templates/default/node_modules/.bin/vite +0 -21
- package/templates/default/node_modules/.bin/yaml +0 -21
- package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/package.json +0 -3
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
- 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.
|
|
3
|
+
"version": "0.2.6",
|
|
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.
|
|
47
|
-
"@grove-dev/core": "0.2.
|
|
48
|
-
"@grove-dev/ui": "0.2.
|
|
46
|
+
"astro": "^6.4.6",
|
|
47
|
+
"@grove-dev/core": "0.2.6",
|
|
48
|
+
"@grove-dev/ui": "0.2.6"
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 {
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
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
|
-
*
|
|
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
|
|
11
|
-
* `
|
|
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 {
|
|
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:
|
|
28
|
+
items: IndexProjectRecord[];
|
|
24
29
|
/** Detail page href builder. */
|
|
25
|
-
hrefFor: (record:
|
|
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.
|
|
19
|
-
*
|
|
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 {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
68
|
-
|
|
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:
|
|
83
|
-
indexRecordsRaw.length > 0 ? indexRecordsRaw :
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
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
|
|
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 {
|
|
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 ??
|
|
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 + (
|
|
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 =
|
|
71
|
-
const pushedAt =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 +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}}
|