@grove-dev/astro 0.1.4 → 0.2.2
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/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppSection.astro +83 -0
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +289 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/.astro/content.d.ts +155 -0
- package/templates/default/.astro/settings.json +5 -0
- package/templates/default/.astro/types.d.ts +2 -0
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +1 -0
- package/templates/default/dist/about/index.html +165 -0
- package/templates/default/dist/icons/platforms/android.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-dark.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-light.svg +1 -0
- package/templates/default/dist/icons/platforms/chrome.svg +1 -0
- package/templates/default/dist/icons/platforms/chromeos.svg +5 -0
- package/templates/default/dist/icons/platforms/desktop.svg +4 -0
- package/templates/default/dist/icons/platforms/embedded.svg +5 -0
- package/templates/default/dist/icons/platforms/ios.svg +1 -0
- package/templates/default/dist/icons/platforms/linux.svg +1 -0
- package/templates/default/dist/icons/platforms/macos.svg +1 -0
- package/templates/default/dist/icons/platforms/ubuntu.svg +1 -0
- package/templates/default/dist/icons/platforms/web.svg +5 -0
- package/templates/default/dist/icons/platforms/windows.svg +6 -0
- package/templates/default/dist/icons/stacks/android.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-dark.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-light.svg +1 -0
- package/templates/default/dist/icons/stacks/capacitor.svg +14 -0
- package/templates/default/dist/icons/stacks/dart.svg +1 -0
- package/templates/default/dist/icons/stacks/firebase.svg +1 -0
- package/templates/default/dist/icons/stacks/flutter.svg +1 -0
- package/templates/default/dist/icons/stacks/graphql.svg +1 -0
- package/templates/default/dist/icons/stacks/ionic.svg +4 -0
- package/templates/default/dist/icons/stacks/java.svg +1 -0
- package/templates/default/dist/icons/stacks/javascript.svg +1 -0
- package/templates/default/dist/icons/stacks/kotlin.svg +1 -0
- package/templates/default/dist/icons/stacks/mongodb.svg +1 -0
- package/templates/default/dist/icons/stacks/nodejs.svg +1 -0
- package/templates/default/dist/icons/stacks/python.svg +1 -0
- package/templates/default/dist/icons/stacks/react-native.svg +1 -0
- package/templates/default/dist/icons/stacks/react.svg +1 -0
- package/templates/default/dist/icons/stacks/rust.svg +1 -0
- package/templates/default/dist/icons/stacks/solidity.svg +1 -0
- package/templates/default/dist/icons/stacks/swift.svg +1 -0
- package/templates/default/dist/icons/stacks/tensorflow.svg +1 -0
- package/templates/default/dist/icons/stacks/typescript.svg +1 -0
- package/templates/default/dist/index.html +55 -0
- package/templates/default/dist/llms-full.txt +14 -0
- package/templates/default/dist/llms.txt +11 -0
- package/templates/default/dist/og-image.svg +48 -0
- package/templates/default/dist/projects/index.html +61 -0
- package/templates/default/dist/robots.txt +53 -0
- package/templates/default/dist/sitemap.xml +21 -0
- package/templates/default/dist/submit/index.html +69 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/node_modules/.bin/astro +21 -0
- package/templates/default/node_modules/.bin/astro-check +21 -0
- package/templates/default/node_modules/.bin/grove +21 -0
- package/templates/default/node_modules/.bin/jiti +21 -0
- package/templates/default/node_modules/.bin/prettier +21 -0
- package/templates/default/node_modules/.bin/rollup +21 -0
- package/templates/default/node_modules/.bin/tsc +21 -0
- package/templates/default/node_modules/.bin/tsserver +21 -0
- package/templates/default/node_modules/.bin/tsx +21 -0
- package/templates/default/node_modules/.bin/vite +21 -0
- package/templates/default/node_modules/.bin/yaml +21 -0
- package/templates/default/node_modules/.vite/deps/_metadata.json +79 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +433 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +9 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +241 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +1592 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +186 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +48 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +6779 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +33 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +88 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +72 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +3757 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +54 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/package.json +3 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +213 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +525 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +1020 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +147 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +7 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +161 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +258 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- package/templates/default/src/pages/contributors.astro +0 -142
|
@@ -1,144 +1,43 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// All links point to /apps with a filter rather than in-page anchors.
|
|
14
|
-
// This keeps the nav usable from any page (e.g. /about) — anchor links
|
|
15
|
-
// would only work on the home page, leaving /apps and /about with dead
|
|
16
|
-
// links. Category/Stack filters are exposed via the /apps refine panel
|
|
17
|
-
// and the home page's "Browse by stack"/"Browse by category" sections.
|
|
18
|
-
const navItems = [
|
|
19
|
-
{ label: "New", href: "/apps?label=new" },
|
|
20
|
-
{ label: "Hot", href: "/apps?label=hot" },
|
|
21
|
-
{ label: "Categories", href: "/apps" },
|
|
22
|
-
{ label: "Stacks", href: "/apps" },
|
|
23
|
-
{ label: "About", href: "/about" },
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const starsLabel = compact(stats.stars);
|
|
2
|
+
import siteConfig from "../../data/generated/site-config.json";
|
|
3
|
+
import { indexSlug } from "../../data/records";
|
|
4
|
+
|
|
5
|
+
const navItems = siteConfig.nav.length > 0
|
|
6
|
+
? siteConfig.nav
|
|
7
|
+
: [
|
|
8
|
+
{ label: "Home", href: "/" },
|
|
9
|
+
{ label: "Browse", href: `/${indexSlug()}` },
|
|
10
|
+
{ label: "About", href: "/about" },
|
|
11
|
+
{ label: "Submit", href: "/submit" },
|
|
12
|
+
];
|
|
27
13
|
---
|
|
28
14
|
|
|
29
|
-
<header
|
|
30
|
-
class="
|
|
31
|
-
>
|
|
32
|
-
<div
|
|
33
|
-
class="mx-auto flex max-w-wide items-center gap-6 px-5 py-3 sm:px-7"
|
|
34
|
-
>
|
|
15
|
+
<header class="border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85">
|
|
16
|
+
<div class="mx-auto flex max-w-6xl items-center gap-6 px-5 py-3 sm:px-7">
|
|
35
17
|
<a
|
|
36
18
|
href="/"
|
|
37
|
-
class="inline-flex items-center gap-2 text-
|
|
19
|
+
class="inline-flex items-center gap-2 text-base font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
38
20
|
>
|
|
39
21
|
<span
|
|
40
22
|
aria-hidden="true"
|
|
41
|
-
class="inline-block h-
|
|
23
|
+
class="inline-block h-4 w-4 rounded bg-ink-900 dark:bg-ink-100"
|
|
42
24
|
></span>
|
|
43
25
|
<span>{siteConfig.name}</span>
|
|
44
26
|
</a>
|
|
45
27
|
|
|
46
28
|
<nav class="hidden flex-1 md:block" aria-label="Primary">
|
|
47
29
|
<ul class="m-0 flex list-none gap-5 p-0">
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
))
|
|
59
|
-
}
|
|
30
|
+
{navItems.map((item) => (
|
|
31
|
+
<li>
|
|
32
|
+
<a
|
|
33
|
+
href={item.href}
|
|
34
|
+
class="text-sm font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
35
|
+
>
|
|
36
|
+
{item.label}
|
|
37
|
+
</a>
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
60
40
|
</ul>
|
|
61
41
|
</nav>
|
|
62
|
-
|
|
63
|
-
<div class="ml-auto flex items-center gap-2">
|
|
64
|
-
<a
|
|
65
|
-
href="/submit"
|
|
66
|
-
class="hidden h-9 items-center gap-1.5 rounded-md border border-ink-200 bg-white px-3 text-[0.875rem] font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 sm:inline-flex dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
|
|
67
|
-
>
|
|
68
|
-
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
|
|
69
|
-
<path d="M7.75 2a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 7.75 2Z"/>
|
|
70
|
-
</svg>
|
|
71
|
-
<span>Submit app</span>
|
|
72
|
-
</a>
|
|
73
|
-
|
|
74
|
-
{/* GitHub button. The star count is rendered as text, not an
|
|
75
|
-
external badge image, so it works offline, doesn't depend
|
|
76
|
-
on shields.io, and reflects what we already know (stats.stars). */}
|
|
77
|
-
<a
|
|
78
|
-
href={githubUrl}
|
|
79
|
-
target="_blank"
|
|
80
|
-
rel="noopener noreferrer"
|
|
81
|
-
aria-label={`View source on GitHub — ${stats.stars} stars`}
|
|
82
|
-
title="View source on GitHub"
|
|
83
|
-
class="inline-flex h-9 items-center divide-x divide-ink-200 overflow-hidden rounded-md border border-ink-200 text-[0.875rem] font-medium transition-colors hover:border-ink-300 hover:bg-ink-50 dark:divide-ink-800 dark:border-ink-800 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
|
|
84
|
-
>
|
|
85
|
-
<span class="flex items-center gap-1.5 px-2.5 py-1.5 text-ink-900 dark:text-ink-100">
|
|
86
|
-
<svg
|
|
87
|
-
viewBox="0 0 16 16"
|
|
88
|
-
width="16"
|
|
89
|
-
height="16"
|
|
90
|
-
aria-hidden="true"
|
|
91
|
-
fill="currentColor"
|
|
92
|
-
>
|
|
93
|
-
<path
|
|
94
|
-
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"
|
|
95
|
-
/>
|
|
96
|
-
</svg>
|
|
97
|
-
<span>Star</span>
|
|
98
|
-
</span>
|
|
99
|
-
<span class="flex items-center gap-1 bg-ink-50 px-2 py-1.5 text-ink-700 dark:bg-ink-900/60 dark:text-ink-300">
|
|
100
|
-
<svg
|
|
101
|
-
viewBox="0 0 16 16"
|
|
102
|
-
width="11"
|
|
103
|
-
height="11"
|
|
104
|
-
aria-hidden="true"
|
|
105
|
-
fill="currentColor"
|
|
106
|
-
class="text-amber-500"
|
|
107
|
-
>
|
|
108
|
-
<path
|
|
109
|
-
d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"
|
|
110
|
-
/>
|
|
111
|
-
</svg>
|
|
112
|
-
<span class="font-mono">{starsLabel}</span>
|
|
113
|
-
</span>
|
|
114
|
-
</a>
|
|
115
|
-
<ThemeToggle />
|
|
116
|
-
</div>
|
|
117
42
|
</div>
|
|
118
43
|
</header>
|
|
119
|
-
|
|
120
|
-
<script>
|
|
121
|
-
// Smooth-scroll for in-page anchor links (Header nav + Hero CTA).
|
|
122
|
-
// Works on any <a href="#…"> that points to an existing element on
|
|
123
|
-
// the current page. External links (e.g. /about, /apps?…) are skipped
|
|
124
|
-
// because they don't start with "#". Modifier-clicks (Cmd/Ctrl/Shift/
|
|
125
|
-
// middle) let the browser handle the anchor normally so users can
|
|
126
|
-
// open the same section in a new tab.
|
|
127
|
-
document.addEventListener("click", (e) => {
|
|
128
|
-
if (e.defaultPrevented) return;
|
|
129
|
-
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
130
|
-
if (e.button !== 0) return; // non-primary mouse button
|
|
131
|
-
const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
|
|
132
|
-
'a[href^="#"]',
|
|
133
|
-
);
|
|
134
|
-
if (!link) return;
|
|
135
|
-
const id = link.getAttribute("href")!;
|
|
136
|
-
if (id.length <= 1) return; // bare "#" → ignore
|
|
137
|
-
const target = document.querySelector(id);
|
|
138
|
-
if (!target) return; // not on this page; let the browser navigate
|
|
139
|
-
e.preventDefault();
|
|
140
|
-
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
141
|
-
// Update the URL hash without an extra jump, so back/forward works.
|
|
142
|
-
history.replaceState(null, "", id);
|
|
143
|
-
});
|
|
144
|
-
</script>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source of truth: `data/generated/records.{full,index,json}`,
|
|
3
|
+
* produced at build time by `grove generate` from
|
|
4
|
+
* `data/records/*.yml`.
|
|
5
|
+
*
|
|
6
|
+
* Three flavors are written by the generator:
|
|
7
|
+
*
|
|
8
|
+
* - `records.full.json` — every record, all visibility. Carries
|
|
9
|
+
* every normalized field (including `content`, `bestFor`,
|
|
10
|
+
* `whyListed`, `caveats`, full `github.repository`, ...).
|
|
11
|
+
* Use this for the detail page.
|
|
12
|
+
* - `records.index.json` — slim projection, visible-only. Use
|
|
13
|
+
* this for the list page and any home-page sectioning.
|
|
14
|
+
* - `records.json` — alias of `records.full.json`.
|
|
15
|
+
*
|
|
16
|
+
* The YML files are the human-edited source; this module is a
|
|
17
|
+
* 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`.
|
|
23
|
+
*
|
|
24
|
+
* When the JSON files are missing (e.g. before `grove generate`
|
|
25
|
+
* runs, or in a fresh scaffold with no records) this module falls
|
|
26
|
+
* back to empty arrays so the Astro build still succeeds — every
|
|
27
|
+
* page must render a graceful "no records yet" state.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { dirname, resolve } from "node:path";
|
|
32
|
+
import type { ProjectRecord, ResourceRecord, EntityRecord, Resource } from "@grove-dev/core";
|
|
33
|
+
|
|
34
|
+
interface RecordsPayload {
|
|
35
|
+
schemaVersion?: number;
|
|
36
|
+
blueprint?: string;
|
|
37
|
+
generatedAt?: string;
|
|
38
|
+
totalRecords?: number;
|
|
39
|
+
visibleRecords?: number;
|
|
40
|
+
records?: Resource[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadGenerated(filename: string): Resource[] {
|
|
44
|
+
// The Astro build resolves JSON imports from `src/data/*.ts`, but
|
|
45
|
+
// we want to be defensive about the order of operations: a fresh
|
|
46
|
+
// scaffold runs `astro build` without first running
|
|
47
|
+
// `grove generate`, in which case the JSON file does not exist.
|
|
48
|
+
// Resolve the path relative to this module's URL, then fall back
|
|
49
|
+
// to an empty list.
|
|
50
|
+
try {
|
|
51
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const candidates = [
|
|
53
|
+
resolve(here, "..", "..", "data", "generated", filename),
|
|
54
|
+
resolve(here, "..", "..", "..", "data", "generated", filename),
|
|
55
|
+
resolve(process.cwd(), "data", "generated", filename),
|
|
56
|
+
];
|
|
57
|
+
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[];
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fullRecordsRaw = loadGenerated("records.full.json");
|
|
68
|
+
const indexRecordsRaw = loadGenerated("records.index.json");
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Full records (all visibility). Use this for the detail page,
|
|
72
|
+
* where you need `content`, `bestFor`, `whyListed`, `caveats`,
|
|
73
|
+
* the full `github.repository` block, etc.
|
|
74
|
+
*/
|
|
75
|
+
export const fullRecords: Resource[] = fullRecordsRaw;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Index-payload records (visible only). Use this for the list
|
|
79
|
+
* page and any home-page sectioning, where you only need the
|
|
80
|
+
* slim search-index fields.
|
|
81
|
+
*/
|
|
82
|
+
export const records: Resource[] =
|
|
83
|
+
indexRecordsRaw.length > 0 ? indexRecordsRaw : fullRecordsRaw;
|
|
84
|
+
|
|
85
|
+
/** Project-kind records only. */
|
|
86
|
+
export const projects = records.filter(
|
|
87
|
+
(r): r is ProjectRecord => r.kind === "project",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/** Resource-kind records. */
|
|
91
|
+
export const resources = records.filter(
|
|
92
|
+
(r): r is ResourceRecord => r.kind === "resource",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/** Entity-kind records. */
|
|
96
|
+
export const entities = records.filter(
|
|
97
|
+
(r): r is EntityRecord => r.kind === "entity",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const bySlug = new Map(fullRecords.map((r) => [r.slug, r]));
|
|
101
|
+
|
|
102
|
+
export function recordBySlug(slug: string): Resource | undefined {
|
|
103
|
+
return bySlug.get(slug);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generic slug lookup. The spec name is `findRecord`; this is an
|
|
108
|
+
* 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
|
+
*/
|
|
114
|
+
export function findRecord(slug: string): Resource | undefined {
|
|
115
|
+
return bySlug.get(slug);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function projectBySlug(slug: string): ProjectRecord | undefined {
|
|
119
|
+
const r = bySlug.get(slug);
|
|
120
|
+
return r && r.kind === "project" ? r : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resourceBySlug(slug: string): ResourceRecord | undefined {
|
|
124
|
+
const r = bySlug.get(slug);
|
|
125
|
+
return r && r.kind === "resource" ? r : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function entityBySlug(slug: string): EntityRecord | undefined {
|
|
129
|
+
const r = bySlug.get(slug);
|
|
130
|
+
return r && r.kind === "entity" ? r : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Page slug for the directory index page, derived from the
|
|
135
|
+
* blueprint in `grove.config.ts` (mirrors the helper that used
|
|
136
|
+
* to live in `data/site-config.ts`).
|
|
137
|
+
*/
|
|
138
|
+
export function indexSlug(blueprint: string | undefined): string {
|
|
139
|
+
switch (blueprint) {
|
|
140
|
+
case "resource-hub":
|
|
141
|
+
return "resources";
|
|
142
|
+
case "ecosystem-map":
|
|
143
|
+
return "entities";
|
|
144
|
+
case "project-directory":
|
|
145
|
+
default:
|
|
146
|
+
return "projects";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Human label for items in the UI. */
|
|
151
|
+
export function itemLabel(blueprint: string | undefined): string {
|
|
152
|
+
switch (blueprint) {
|
|
153
|
+
case "resource-hub":
|
|
154
|
+
return "resource";
|
|
155
|
+
case "ecosystem-map":
|
|
156
|
+
return "entity";
|
|
157
|
+
case "project-directory":
|
|
158
|
+
default:
|
|
159
|
+
return "project";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny markdown → HTML converter for page-level content bodies.
|
|
3
|
+
*
|
|
4
|
+
* Scope is intentionally narrow: it handles the markdown the
|
|
5
|
+
* `content/pages/*.md` files actually use (headings, paragraphs,
|
|
6
|
+
* unordered + ordered lists, fenced code, inline `code` and
|
|
7
|
+
* links). Anything more elaborate (tables, footnotes, autolinks)
|
|
8
|
+
* is out of scope — consumers can swap in a real renderer
|
|
9
|
+
* (marked, remark, ...) by overriding the page-level helper.
|
|
10
|
+
*
|
|
11
|
+
* The output is HTML-escaped at the source so a stray `<` in the
|
|
12
|
+
* markdown file can't break the page. Inline `code` and links are
|
|
13
|
+
* processed before the escape pass.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
|
|
17
|
+
function escapeHtml(s: string): string {
|
|
18
|
+
return s
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderInline(line: string): string {
|
|
25
|
+
let out = escapeHtml(line);
|
|
26
|
+
// Inline code: `…` — render first so its content doesn't get
|
|
27
|
+
// further processed.
|
|
28
|
+
out = out.replace(/`([^`]+)`/g, (_, code: string) => `<code>${code}</code>`);
|
|
29
|
+
// Links: [text](url)
|
|
30
|
+
out = out.replace(
|
|
31
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
32
|
+
(_, text: string, url: string) =>
|
|
33
|
+
`<a href="${url}" class="text-ink-900 underline hover:text-ink-700 dark:text-ink-100 dark:hover:text-white" rel="noopener noreferrer">${text}</a>`,
|
|
34
|
+
);
|
|
35
|
+
// Bold: **…**
|
|
36
|
+
out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
37
|
+
// Italic: *…*
|
|
38
|
+
out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Block {
|
|
43
|
+
type: "h2" | "h3" | "p" | "ul" | "ol" | "pre";
|
|
44
|
+
lines: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseBlocks(md: string): Block[] {
|
|
48
|
+
const lines = md.split(/\r?\n/);
|
|
49
|
+
const blocks: Block[] = [];
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < lines.length) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
if (!line.trim()) {
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (line.startsWith("## ")) {
|
|
58
|
+
blocks.push({ type: "h2", lines: [line.slice(3)] });
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (line.startsWith("### ")) {
|
|
63
|
+
blocks.push({ type: "h3", lines: [line.slice(4)] });
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.startsWith("```")) {
|
|
68
|
+
const buf: string[] = [];
|
|
69
|
+
i++;
|
|
70
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
71
|
+
buf.push(lines[i]);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
i++; // skip closing fence
|
|
75
|
+
blocks.push({ type: "pre", lines: buf });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^[-*] /.test(line)) {
|
|
79
|
+
const buf: string[] = [];
|
|
80
|
+
while (i < lines.length && /^[-*] /.test(lines[i])) {
|
|
81
|
+
buf.push(lines[i].slice(2));
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
blocks.push({ type: "ul", lines: buf });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (/^\d+\. /.test(line)) {
|
|
88
|
+
const buf: string[] = [];
|
|
89
|
+
while (i < lines.length && /^\d+\. /.test(lines[i])) {
|
|
90
|
+
buf.push(lines[i].replace(/^\d+\. /, ""));
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
blocks.push({ type: "ol", lines: buf });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Default: paragraph (consume contiguous non-blank lines).
|
|
97
|
+
const buf: string[] = [line];
|
|
98
|
+
i++;
|
|
99
|
+
while (
|
|
100
|
+
i < lines.length &&
|
|
101
|
+
lines[i].trim() &&
|
|
102
|
+
!lines[i].startsWith("## ") &&
|
|
103
|
+
!lines[i].startsWith("### ") &&
|
|
104
|
+
!lines[i].startsWith("```") &&
|
|
105
|
+
!/^[-*] /.test(lines[i]) &&
|
|
106
|
+
!/^\d+\. /.test(lines[i])
|
|
107
|
+
) {
|
|
108
|
+
buf.push(lines[i]);
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
blocks.push({ type: "p", lines: buf });
|
|
112
|
+
}
|
|
113
|
+
return blocks;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderMarkdown(md: string): string {
|
|
117
|
+
const blocks = parseBlocks(md);
|
|
118
|
+
const html: string[] = [];
|
|
119
|
+
for (const b of blocks) {
|
|
120
|
+
switch (b.type) {
|
|
121
|
+
case "h2":
|
|
122
|
+
html.push(
|
|
123
|
+
`<h2 class="m-0 mt-10 text-[1.375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h2>`,
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
case "h3":
|
|
127
|
+
html.push(
|
|
128
|
+
`<h3 class="m-0 mt-8 text-[1.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h3>`,
|
|
129
|
+
);
|
|
130
|
+
break;
|
|
131
|
+
case "p":
|
|
132
|
+
html.push(
|
|
133
|
+
`<p class="m-0 mt-4 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines.map(renderInline).join(" ")}</p>`,
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
case "ul":
|
|
137
|
+
html.push(
|
|
138
|
+
`<ul class="m-0 mt-4 list-disc space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
|
|
139
|
+
.map((l) => `<li>${renderInline(l)}</li>`)
|
|
140
|
+
.join("")}</ul>`,
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
case "ol":
|
|
144
|
+
html.push(
|
|
145
|
+
`<ol class="m-0 mt-4 list-decimal space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
|
|
146
|
+
.map((l) => `<li>${renderInline(l)}</li>`)
|
|
147
|
+
.join("")}</ol>`,
|
|
148
|
+
);
|
|
149
|
+
break;
|
|
150
|
+
case "pre":
|
|
151
|
+
html.push(
|
|
152
|
+
`<pre class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code>${escapeHtml(b.lines.join("\n"))}</code></pre>`,
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return html.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Read a markdown file from a list of candidate paths, return null if missing. */
|
|
161
|
+
export function readMarkdownFile(candidates: string[]): string | null {
|
|
162
|
+
for (const path of candidates) {
|
|
163
|
+
try {
|
|
164
|
+
if (!existsSync(path)) continue;
|
|
165
|
+
return readFileSync(path, "utf8");
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|