@grove-dev/astro 0.2.10 → 0.2.13
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/lib/display.d.ts +11 -17
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +43 -43
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +6 -7
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +11 -12
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +35 -73
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +144 -126
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +11 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +12 -25
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +15 -16
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +4 -11
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +47 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +195 -180
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +7 -21
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +12 -31
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +4 -4
- package/src/components/AppsIndexRow.astro +55 -184
- package/src/components/AppsPagination.astro +45 -108
- package/src/components/CategoryGrid.astro +43 -31
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +41 -0
- package/src/components/ExploreByStack.astro +6 -0
- package/src/components/FilterGroupMenu.astro +49 -0
- package/src/components/FilterOptions.astro +44 -0
- package/src/components/Hero.astro +198 -147
- package/src/components/Icon.astro +10 -3
- package/src/components/ItemCard.astro +266 -248
- package/src/components/RecordSection.astro +68 -33
- package/src/components/RefinePanel.astro +115 -141
- package/src/components/StackGrid.astro +32 -24
- package/src/components/WhyThisExists.astro +32 -32
- package/src/layouts/BaseLayout.astro +104 -49
- package/src/layouts/Container.astro +5 -44
- package/src/layouts/Footer.astro +31 -20
- package/src/layouts/Header.astro +40 -19
- package/src/layouts/SectionHeader.astro +27 -43
- package/src/layouts/Seo.astro +38 -56
- package/src/layouts/ThemeToggle.astro +35 -48
- package/src/lib/display.ts +46 -53
- package/src/lib/format.ts +11 -12
- package/src/lib/lenses.ts +151 -172
- package/src/lib/repo.ts +16 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +222 -290
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +207 -1
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
- package/templates/default/.github/pull_request_template.md +27 -0
- package/templates/default/.github/workflows/build.yml +55 -0
- package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
- package/templates/default/.github/workflows/daily-refresh.yml +53 -0
- package/templates/default/.github/workflows/sync-contributors.yml +53 -0
- package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
- package/templates/default/.github/workflows/validate-data.yml +35 -0
- package/templates/default/data/records/coolify.yml +46 -0
- package/templates/default/data/records/gitea.yml +46 -0
- package/templates/default/data/records/hoppscotch.yml +47 -0
- package/templates/default/data/records/inventree.yml +47 -0
- package/templates/default/data/records/label-studio.yml +46 -0
- package/templates/default/data/records/logseq.yml +48 -0
- package/templates/default/data/records/private-gpt.yml +47 -0
- package/templates/default/data/records/wakapi.yml +44 -0
- package/templates/default/data/taxonomy/categories.yml +47 -0
- package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
- package/templates/default/data/taxonomy/platforms.yml +27 -0
- package/templates/default/data/taxonomy/stacks.yml +41 -0
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +5 -4
- package/templates/default/public/icons/stacks/bun.svg +1 -0
- package/templates/default/public/icons/stacks/clojure.svg +1 -0
- package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
- package/templates/default/public/icons/stacks/deno.svg +1 -0
- package/templates/default/public/icons/stacks/django.svg +1 -0
- package/templates/default/public/icons/stacks/docker.svg +1 -0
- package/templates/default/public/icons/stacks/go.svg +1 -0
- package/templates/default/public/icons/stacks/llm.svg +1 -0
- package/templates/default/public/icons/stacks/node.js.svg +1 -0
- package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
- package/templates/default/public/icons/stacks/vue.svg +1 -0
- package/templates/default/public/llms-full.txt +197 -5
- package/templates/default/public/llms.txt +107 -6
- package/templates/default/public/sitemap.xml +63 -0
- package/templates/default/scripts/build-llms.mjs +123 -0
- package/templates/default/scripts/build-records-json.mjs +27 -0
- package/templates/default/scripts/build-sitemap.mjs +20 -0
- package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
- package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
- package/templates/default/scripts/fetch-icons.mjs +137 -0
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
- package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
- package/templates/default/scripts/refresh-records-activity.mjs +24 -0
- package/templates/default/scripts/repair-license-format.mjs +72 -0
- package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
- package/templates/default/scripts/seed-from-github.mjs +62 -0
- package/templates/default/scripts/sync-contributors.mjs +145 -0
- package/templates/default/scripts/sync-github-metadata.mjs +28 -0
- package/templates/default/scripts/validate-records.mjs +27 -0
- package/templates/default/src/data/records.ts +134 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +476 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
- package/templates/default/src/pages/apps/index.astro +146 -0
- package/templates/default/src/pages/contributors.astro +184 -0
- package/templates/default/src/pages/index.astro +119 -162
- package/templates/default/src/pages/submit.astro +276 -114
- package/templates/default/wrangler.jsonc +28 -0
- package/LICENSE +0 -21
- 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 -1
- package/templates/default/data/generated/apps.json +0 -1
- package/templates/default/data/generated/records.full.json +0 -8
- package/templates/default/data/generated/records.index.json +0 -7
- package/templates/default/data/generated/records.json +0 -8
- package/templates/default/data/generated/repo-stats.json +0 -1
- package/templates/default/data/generated/site-config.json +0 -31
- package/templates/default/dist/_astro/BaseLayout.Dz_HR-68.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/templates/default/src/components/layout/BaseLayout.astro +0 -47
- package/templates/default/src/components/layout/Container.astro +0 -18
- package/templates/default/src/components/layout/Footer.astro +0 -27
- package/templates/default/src/components/layout/Header.astro +0 -43
- package/templates/default/src/lib/markdown.ts +0 -171
- package/templates/default/src/pages/projects/index.astro +0 -193
|
@@ -1,58 +1,42 @@
|
|
|
1
1
|
---
|
|
2
|
-
/**
|
|
3
|
-
* SectionHeader — eyebrow + title + description, used to title
|
|
4
|
-
* every section on the home page. Optionally centered (good for
|
|
5
|
-
* "Why this exists" and friends).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
2
|
interface Props {
|
|
9
3
|
eyebrow?: string;
|
|
10
4
|
title: string;
|
|
11
5
|
description?: string;
|
|
12
6
|
align?: "left" | "center";
|
|
13
|
-
/** 2 (default) for top-level sections, 3 for nested ones. */
|
|
14
7
|
level?: 2 | 3;
|
|
15
|
-
/** Extra class for the outer header element. */
|
|
16
|
-
class?: string;
|
|
17
8
|
}
|
|
18
9
|
|
|
19
|
-
const {
|
|
20
|
-
eyebrow,
|
|
21
|
-
title,
|
|
22
|
-
description,
|
|
23
|
-
align = "left",
|
|
24
|
-
level = 2,
|
|
25
|
-
class: className = "",
|
|
26
|
-
} = Astro.props;
|
|
10
|
+
const { eyebrow, title, description, align = "left", level = 2 } = Astro.props;
|
|
27
11
|
const Heading = `h${level}` as "h2" | "h3";
|
|
28
12
|
---
|
|
29
13
|
|
|
30
14
|
<header
|
|
31
|
-
class
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
className,
|
|
35
|
-
]}
|
|
15
|
+
class={`mb-6 flex flex-col gap-2 ${
|
|
16
|
+
align === "center" ? "items-center text-center" : ""
|
|
17
|
+
}`}
|
|
36
18
|
>
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
19
|
+
{
|
|
20
|
+
eyebrow && (
|
|
21
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
22
|
+
{eyebrow}
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
<Heading
|
|
27
|
+
class="text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
|
|
28
|
+
>
|
|
29
|
+
{title}
|
|
30
|
+
</Heading>
|
|
31
|
+
{
|
|
32
|
+
description && (
|
|
33
|
+
<p
|
|
34
|
+
class={`text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 ${
|
|
35
|
+
align === "center" ? "max-w-2xl" : "max-w-[56ch]"
|
|
36
|
+
}`}
|
|
37
|
+
>
|
|
38
|
+
{description}
|
|
39
|
+
</p>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
58
42
|
</header>
|
package/src/layouts/Seo.astro
CHANGED
|
@@ -1,80 +1,73 @@
|
|
|
1
1
|
---
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* derived from `site` and the Astro URL.
|
|
5
|
+
* Per-page SEO: <title>, description, canonical, Open Graph, Twitter Card,
|
|
6
|
+
* and JSON-LD structured data. Designed to be used inside <head> by the
|
|
7
|
+
* BaseLayout. Pages pass the rich props they care about; everything else
|
|
8
|
+
* has a sensible default derived from the site config and the Astro URL.
|
|
8
9
|
*
|
|
9
10
|
* Renders:
|
|
10
11
|
* - <title>, meta description, canonical link
|
|
11
12
|
* - Open Graph (og:*) for Facebook, LinkedIn, Discord, Slack, etc.
|
|
12
13
|
* - Twitter Card (twitter:*) for X / Twitter
|
|
13
14
|
* - JSON-LD WebSite + Organization on every page
|
|
14
|
-
* - JSON-LD
|
|
15
|
-
*
|
|
15
|
+
* - JSON-LD SoftwareSourceCode (or custom) when `jsonLd` is provided
|
|
16
|
+
* (e.g. on /items/[slug] for the per-item structured data)
|
|
16
17
|
*
|
|
17
|
-
* Noindex is opt-in via the `noindex` prop. By default everything
|
|
18
|
-
* is
|
|
18
|
+
* Noindex is opt-in via the `noindex` prop. By default everything is
|
|
19
|
+
* indexable; the only noindex target today is the thin /submit wrapper.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
interface SiteRef {
|
|
22
|
-
name: string;
|
|
23
|
-
description?: string;
|
|
24
|
-
url?: string;
|
|
25
|
-
repoUrl?: string;
|
|
26
|
-
searchPath?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
interface Props {
|
|
30
23
|
title: string;
|
|
31
24
|
description: string;
|
|
32
|
-
|
|
25
|
+
site: {
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
repoUrl?: string;
|
|
29
|
+
};
|
|
30
|
+
searchPath?: string;
|
|
31
|
+
/** Open Graph / Twitter image. Default: /og-image.svg. Should be absolute or root-relative. */
|
|
33
32
|
image?: string;
|
|
34
|
-
/** og:type. Default: "website". */
|
|
33
|
+
/** og:type. Default: "website". Use "article" for blog posts / changelog. */
|
|
35
34
|
type?: "website" | "article" | "profile";
|
|
36
35
|
/** Set true to add noindex,nofollow. */
|
|
37
36
|
noindex?: boolean;
|
|
38
|
-
/** Extra JSON-LD payload (object) — typically SoftwareSourceCode. */
|
|
37
|
+
/** Extra JSON-LD payload (object) — typically SoftwareSourceCode on item pages. */
|
|
39
38
|
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
40
|
-
/** Site identity (name, description, url, repoUrl). Required. */
|
|
41
|
-
site: SiteRef;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
const {
|
|
45
42
|
title,
|
|
46
43
|
description,
|
|
44
|
+
site: siteConfig,
|
|
45
|
+
searchPath = "/items",
|
|
47
46
|
image = "/og-image.svg",
|
|
48
47
|
type = "website",
|
|
49
48
|
noindex = false,
|
|
50
49
|
jsonLd,
|
|
51
|
-
site,
|
|
52
50
|
} = Astro.props;
|
|
53
51
|
|
|
54
|
-
const siteUrl = site.
|
|
55
|
-
? new URL(site.url)
|
|
56
|
-
: (Astro.site ?? new URL("https://grove.dev"));
|
|
52
|
+
const siteUrl = Astro.site ?? new URL("https://example.com");
|
|
57
53
|
const canonical = new URL(Astro.url.pathname, siteUrl).toString();
|
|
58
54
|
const ogImage = new URL(image, siteUrl).toString();
|
|
59
55
|
|
|
60
|
-
// Truncate description for OG/Twitter where longer is worse (160
|
|
56
|
+
// Truncate description for OG/Twitter where longer is worse (160–200 chars).
|
|
61
57
|
function truncate(s: string, max: number): string {
|
|
62
58
|
if (s.length <= max) return s;
|
|
63
59
|
return s.slice(0, max - 1).trimEnd() + "\u2026";
|
|
64
60
|
}
|
|
65
61
|
const shortDescription = truncate(description, 200);
|
|
66
|
-
const siteDescription =
|
|
67
|
-
site.description ??
|
|
68
|
-
"A growing community knowledge site powered by Grove.";
|
|
69
62
|
|
|
70
|
-
// JSON-LD
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const websiteLd
|
|
63
|
+
// WebSite + Organization JSON-LD. These are site-wide identity signals
|
|
64
|
+
// that should appear on every page (Google uses them for site links
|
|
65
|
+
// and the knowledge panel).
|
|
66
|
+
const websiteLd = {
|
|
74
67
|
"@context": "https://schema.org",
|
|
75
68
|
"@type": "WebSite",
|
|
76
|
-
name:
|
|
77
|
-
description:
|
|
69
|
+
name: siteConfig.name,
|
|
70
|
+
description: siteConfig.description ?? description,
|
|
78
71
|
url: siteUrl.toString().replace(/\/$/, ""),
|
|
79
72
|
inLanguage: "en",
|
|
80
73
|
potentialAction: {
|
|
@@ -84,17 +77,14 @@ const websiteLd: Record<string, unknown> = {
|
|
|
84
77
|
},
|
|
85
78
|
};
|
|
86
79
|
|
|
87
|
-
const organizationLd
|
|
80
|
+
const organizationLd = {
|
|
88
81
|
"@context": "https://schema.org",
|
|
89
82
|
"@type": "Organization",
|
|
90
|
-
name:
|
|
83
|
+
name: siteConfig.name,
|
|
91
84
|
url: siteUrl.toString().replace(/\/$/, ""),
|
|
92
85
|
logo: new URL("/og-image.svg", siteUrl).toString(),
|
|
86
|
+
...(siteConfig.repoUrl ? { sameAs: [siteConfig.repoUrl] } : {}),
|
|
93
87
|
};
|
|
94
|
-
|
|
95
|
-
if (site.repoUrl) {
|
|
96
|
-
organizationLd.sameAs = [site.repoUrl];
|
|
97
|
-
}
|
|
98
88
|
---
|
|
99
89
|
|
|
100
90
|
<!-- Primary meta -->
|
|
@@ -111,8 +101,8 @@ if (site.repoUrl) {
|
|
|
111
101
|
<meta property="og:image" content={ogImage} />
|
|
112
102
|
<meta property="og:image:width" content="1200" />
|
|
113
103
|
<meta property="og:image:height" content="630" />
|
|
114
|
-
<meta property="og:image:alt" content={`${
|
|
115
|
-
<meta property="og:site_name" content={
|
|
104
|
+
<meta property="og:image:alt" content={`${siteConfig.name} social preview`} />
|
|
105
|
+
<meta property="og:site_name" content={siteConfig.name} />
|
|
116
106
|
<meta property="og:locale" content="en" />
|
|
117
107
|
|
|
118
108
|
<!-- Twitter Card -->
|
|
@@ -120,21 +110,13 @@ if (site.repoUrl) {
|
|
|
120
110
|
<meta name="twitter:title" content={title} />
|
|
121
111
|
<meta name="twitter:description" content={shortDescription} />
|
|
122
112
|
<meta name="twitter:image" content={ogImage} />
|
|
123
|
-
<meta name="twitter:image:alt" content={`${
|
|
113
|
+
<meta name="twitter:image:alt" content={`${siteConfig.name} social preview`} />
|
|
124
114
|
|
|
125
115
|
<!-- JSON-LD: site-wide identity -->
|
|
126
|
-
<script
|
|
127
|
-
|
|
128
|
-
type="application/ld+json"
|
|
129
|
-
set:html={JSON.stringify(websiteLd)}
|
|
130
|
-
></script>
|
|
131
|
-
<script
|
|
132
|
-
is:inline
|
|
133
|
-
type="application/ld+json"
|
|
134
|
-
set:html={JSON.stringify(organizationLd)}
|
|
135
|
-
></script>
|
|
116
|
+
<script is:inline type="application/ld+json" set:html={JSON.stringify(websiteLd)} />
|
|
117
|
+
<script is:inline type="application/ld+json" set:html={JSON.stringify(organizationLd)} />
|
|
136
118
|
|
|
137
|
-
<!-- JSON-LD: per-page (SoftwareSourceCode for [slug], etc.) -->
|
|
119
|
+
<!-- JSON-LD: per-page (SoftwareSourceCode for /items/[slug], etc.) -->
|
|
138
120
|
{jsonLd && (
|
|
139
121
|
<script
|
|
140
122
|
is:inline
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* `theme:set` event that the THEME_INIT listener picks up.
|
|
14
|
-
*/
|
|
2
|
+
// A three-position theme switcher: light / dark / system.
|
|
3
|
+
//
|
|
4
|
+
// State is persisted in `localStorage` under the key "grove-theme".
|
|
5
|
+
// On mount it reads the stored value (or "system" if none), applies
|
|
6
|
+
// the corresponding class to <html>, and re-syncs when the OS
|
|
7
|
+
// preference changes (only meaningful when mode is "system").
|
|
8
|
+
//
|
|
9
|
+
// Implementation note: the actual <html class="dark"> toggle happens
|
|
10
|
+
// in the inline script in `src/components/layout/BaseLayout.astro`
|
|
11
|
+
// (THEME_INIT). That script runs before paint so the user never sees
|
|
12
|
+
// a flash of the wrong theme.
|
|
15
13
|
|
|
16
14
|
interface Props {
|
|
17
15
|
class?: string;
|
|
@@ -22,17 +20,15 @@ const { class: className = "" } = Astro.props;
|
|
|
22
20
|
|
|
23
21
|
<button
|
|
24
22
|
type="button"
|
|
23
|
+
id="theme-toggle"
|
|
25
24
|
data-theme-toggle
|
|
26
25
|
aria-label="Toggle theme"
|
|
27
26
|
title="Toggle theme"
|
|
28
|
-
class
|
|
29
|
-
"relative inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200 dark:hover:border-ink-700 dark:hover:bg-ink-900/40",
|
|
30
|
-
className,
|
|
31
|
-
]}
|
|
27
|
+
class={`relative inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200 dark:hover:border-ink-700 dark:hover:bg-ink-900/40 ${className}`}
|
|
32
28
|
>
|
|
33
29
|
{/* Sun — shown in dark mode (click to go light) */}
|
|
34
30
|
<svg
|
|
35
|
-
class="
|
|
31
|
+
class="h-4 w-4 hidden dark:block"
|
|
36
32
|
viewBox="0 0 24 24"
|
|
37
33
|
fill="none"
|
|
38
34
|
stroke="currentColor"
|
|
@@ -41,12 +37,12 @@ const { class: className = "" } = Astro.props;
|
|
|
41
37
|
stroke-linejoin="round"
|
|
42
38
|
aria-hidden="true"
|
|
43
39
|
>
|
|
44
|
-
<circle cx="12" cy="12" r="4"
|
|
45
|
-
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
|
|
40
|
+
<circle cx="12" cy="12" r="4"/>
|
|
41
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
|
46
42
|
</svg>
|
|
47
43
|
{/* Moon — shown in light mode (click to go dark) */}
|
|
48
44
|
<svg
|
|
49
|
-
class="
|
|
45
|
+
class="h-4 w-4 block dark:hidden"
|
|
50
46
|
viewBox="0 0 24 24"
|
|
51
47
|
fill="none"
|
|
52
48
|
stroke="currentColor"
|
|
@@ -55,7 +51,7 @@ const { class: className = "" } = Astro.props;
|
|
|
55
51
|
stroke-linejoin="round"
|
|
56
52
|
aria-hidden="true"
|
|
57
53
|
>
|
|
58
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"
|
|
54
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/>
|
|
59
55
|
</svg>
|
|
60
56
|
</button>
|
|
61
57
|
|
|
@@ -67,44 +63,35 @@ const { class: className = "" } = Astro.props;
|
|
|
67
63
|
type Mode = "light" | "dark" | "system";
|
|
68
64
|
|
|
69
65
|
function currentMode(): Mode {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
: "system";
|
|
75
|
-
} catch {
|
|
76
|
-
return "system";
|
|
77
|
-
}
|
|
66
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
67
|
+
return stored === "light" || stored === "dark" || stored === "system"
|
|
68
|
+
? stored
|
|
69
|
+
: "system";
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
function rotate(): Mode {
|
|
81
73
|
const order: Mode[] = ["light", "dark", "system"];
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
localStorage.setItem(STORAGE_KEY, next);
|
|
86
|
-
} catch {
|
|
87
|
-
// localStorage may be blocked (private mode, etc.) — we still
|
|
88
|
-
// dispatch the event so the UI updates for the current page.
|
|
89
|
-
}
|
|
90
|
-
// Notify the page-level listener (THEME_INIT).
|
|
74
|
+
const next = order[(order.indexOf(currentMode()) + 1) % order.length];
|
|
75
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
76
|
+
// Notify the page-level listener.
|
|
91
77
|
window.dispatchEvent(new CustomEvent("theme:set", { detail: next }));
|
|
92
78
|
return next;
|
|
93
79
|
}
|
|
94
80
|
|
|
81
|
+
// A small tooltip label so the user knows what mode they'll get
|
|
82
|
+
// on the next click. We re-read current mode on focus/hover so it
|
|
83
|
+
// stays in sync.
|
|
95
84
|
function updateLabel(btn: HTMLButtonElement) {
|
|
96
85
|
btn.title = `Theme: ${currentMode()} (click to switch)`;
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
document
|
|
100
|
-
|
|
101
|
-
.
|
|
88
|
+
document.querySelectorAll<HTMLButtonElement>("[data-theme-toggle]").forEach((btn) => {
|
|
89
|
+
updateLabel(btn);
|
|
90
|
+
btn.addEventListener("click", () => {
|
|
91
|
+
rotate();
|
|
102
92
|
updateLabel(btn);
|
|
103
|
-
btn.addEventListener("click", () => {
|
|
104
|
-
rotate();
|
|
105
|
-
updateLabel(btn);
|
|
106
|
-
});
|
|
107
|
-
btn.addEventListener("mouseenter", () => updateLabel(btn));
|
|
108
|
-
btn.addEventListener("focus", () => updateLabel(btn));
|
|
109
93
|
});
|
|
94
|
+
btn.addEventListener("mouseenter", () => updateLabel(btn));
|
|
95
|
+
btn.addEventListener("focus", () => updateLabel(btn));
|
|
96
|
+
});
|
|
110
97
|
</script>
|
package/src/lib/display.ts
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Display-name helpers for curation labels, statuses, lenses, and
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Display-name helpers for curation labels, statuses, lenses, and sort
|
|
3
|
+
* options. Internal ids (e.g. "hot", "stale", "production-like") stay
|
|
4
|
+
* stable for URL state and data files; this module owns the mapping
|
|
5
|
+
* from id → user-facing string.
|
|
6
6
|
*
|
|
7
|
-
* Every export is dependency-free so it can be imported by both
|
|
7
|
+
* Every export here is dependency-free so it can be imported by both
|
|
8
8
|
* server-rendered Astro components and any client-side script.
|
|
9
|
-
*
|
|
10
|
-
* Status ids accept the `HealthStatus` union from `@grove-dev/core`
|
|
11
|
-
* plus a couple of curated composites (e.g. "needs-maintainer")
|
|
12
|
-
* that appear in URL state.
|
|
13
9
|
*/
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
HealthStatus,
|
|
18
|
-
} from "@grove-dev/core";
|
|
19
|
-
|
|
20
|
-
// ── Label ids (the `curation.labels` array on a record) ──────────
|
|
21
|
-
export type LabelId = AppLabel;
|
|
11
|
+
// ── Label ids (the `labels` array on an item) ────────────────────────
|
|
12
|
+
export type LabelId = "new" | "hot" | "mature" | "featured";
|
|
22
13
|
|
|
23
14
|
export const LABEL_DISPLAY: Record<LabelId, string> = {
|
|
24
15
|
new: "Recently added",
|
|
@@ -27,70 +18,58 @@ export const LABEL_DISPLAY: Record<LabelId, string> = {
|
|
|
27
18
|
featured: "Featured",
|
|
28
19
|
};
|
|
29
20
|
|
|
30
|
-
/** Pretty-print a slug to Title Case. Used for arbitrary slugs. */
|
|
31
|
-
export function prettySlug(slug: string | null | undefined): string {
|
|
32
|
-
if (!slug) return "";
|
|
33
|
-
return slug
|
|
34
|
-
.replace(/[-_]+/g, " ")
|
|
35
|
-
.replace(/\s+/g, " ")
|
|
36
|
-
.trim()
|
|
37
|
-
.split(" ")
|
|
38
|
-
.map((w) => (w ? w[0]!.toUpperCase() + w.slice(1) : ""))
|
|
39
|
-
.join(" ");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
21
|
/** Returns a human-readable name for a label id, or null if unknown. */
|
|
43
22
|
export function labelDisplay(id: string | null | undefined): string | null {
|
|
44
23
|
if (!id) return null;
|
|
45
|
-
|
|
46
|
-
return LABEL_DISPLAY[id as LabelId];
|
|
47
|
-
}
|
|
48
|
-
return prettySlug(id);
|
|
24
|
+
return LABEL_DISPLAY[id as LabelId] ?? null;
|
|
49
25
|
}
|
|
50
26
|
|
|
51
|
-
// ── Status ids (the `
|
|
27
|
+
// ── Status ids (the `status` field on an item) ───────────────────────
|
|
52
28
|
/**
|
|
53
|
-
* Canonical status
|
|
54
|
-
*
|
|
55
|
-
*
|
|
29
|
+
* Canonical status ids, including the curated "needs-maintainer"
|
|
30
|
+
* composite that's emitted by the `needs-maintainer` lens (it expands
|
|
31
|
+
* to "stale,quiet" in URL state but presents as one chip).
|
|
56
32
|
*/
|
|
57
33
|
export const STATUS_DISPLAY: Record<string, string> = {
|
|
58
34
|
active: "Active",
|
|
59
35
|
mature: "Mature",
|
|
36
|
+
quiet: "Quiet",
|
|
60
37
|
stale: "Not recently active",
|
|
61
38
|
inactive: "Inactive",
|
|
39
|
+
"needs-maintainer": "Needs maintainer",
|
|
62
40
|
archived: "Archived",
|
|
63
|
-
unknown: "Unknown",
|
|
64
41
|
historical: "Historical",
|
|
65
42
|
needs_review: "Needs review",
|
|
66
|
-
quiet: "Quiet",
|
|
67
43
|
unavailable: "Unavailable",
|
|
68
|
-
|
|
44
|
+
unknown: "Unknown",
|
|
69
45
|
};
|
|
70
46
|
|
|
71
|
-
export function statusDisplay(
|
|
72
|
-
s: HealthStatus | string | null | undefined,
|
|
73
|
-
): string {
|
|
47
|
+
export function statusDisplay(s: string | null | undefined): string {
|
|
74
48
|
if (!s) return STATUS_DISPLAY.unknown;
|
|
75
|
-
return STATUS_DISPLAY[s] ??
|
|
49
|
+
return STATUS_DISPLAY[s] ?? STATUS_DISPLAY.unknown;
|
|
76
50
|
}
|
|
77
51
|
|
|
78
|
-
// ── Lens ids (the curated tabs on /
|
|
52
|
+
// ── Lens ids (the curated tabs on /items) ────────────────────────────
|
|
79
53
|
export const LENS_DISPLAY: Record<string, string> = {
|
|
80
|
-
all: "All",
|
|
81
|
-
hot: "Trending",
|
|
54
|
+
all: "All items",
|
|
82
55
|
new: "Recently added",
|
|
56
|
+
hot: "Trending",
|
|
83
57
|
mature: "Established",
|
|
84
|
-
|
|
85
|
-
"
|
|
58
|
+
"good-to-learn": "Good to learn",
|
|
59
|
+
"production-like": "Production-like",
|
|
60
|
+
"beginner-friendly": "Beginner friendly",
|
|
61
|
+
"contribution-ready": "Contribution ready",
|
|
62
|
+
launches: "Launches",
|
|
63
|
+
"actively-developed": "Active",
|
|
64
|
+
"needs-maintainer": "Needs maintainer",
|
|
86
65
|
};
|
|
87
66
|
|
|
88
67
|
export function lensDisplay(id: string | null | undefined): string {
|
|
89
68
|
if (!id) return LENS_DISPLAY.all;
|
|
90
|
-
return LENS_DISPLAY[id] ??
|
|
69
|
+
return LENS_DISPLAY[id] ?? id;
|
|
91
70
|
}
|
|
92
71
|
|
|
93
|
-
// ── Sort ids (the /
|
|
72
|
+
// ── Sort ids (the /items sort dropdown) ──────────────────────────────
|
|
94
73
|
export const SORT_DISPLAY: Record<string, string> = {
|
|
95
74
|
"recently-updated": "Recently updated",
|
|
96
75
|
"most-starred": "Most starred",
|
|
@@ -101,18 +80,32 @@ export const SORT_DISPLAY: Record<string, string> = {
|
|
|
101
80
|
|
|
102
81
|
export function sortDisplay(id: string | null | undefined): string {
|
|
103
82
|
if (!id) return SORT_DISPLAY["recently-updated"];
|
|
104
|
-
return SORT_DISPLAY[id] ??
|
|
83
|
+
return SORT_DISPLAY[id] ?? id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function prettySlug(value: string | null | undefined): string {
|
|
87
|
+
if (!value) return "";
|
|
88
|
+
return value
|
|
89
|
+
.split(/[-_]+/)
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
92
|
+
.join(" ");
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
// ── Status option list (used by filter dropdowns)
|
|
95
|
+
// ── Status option list (used by filter dropdowns) ───────────────────
|
|
108
96
|
// "needs-maintainer" is included even though it's a derived/composite
|
|
109
97
|
// status — filter dropdowns and chip rendering need to offer it
|
|
110
|
-
// alongside the raw
|
|
98
|
+
// alongside the raw AppStatus values.
|
|
111
99
|
export const STATUS_OPTIONS: string[] = [
|
|
112
100
|
"active",
|
|
101
|
+
"mature",
|
|
113
102
|
"quiet",
|
|
114
103
|
"stale",
|
|
104
|
+
"inactive",
|
|
115
105
|
"needs-maintainer",
|
|
116
106
|
"archived",
|
|
107
|
+
"historical",
|
|
108
|
+
"needs_review",
|
|
109
|
+
"unavailable",
|
|
117
110
|
"unknown",
|
|
118
111
|
];
|
package/src/lib/format.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting helpers.
|
|
2
|
+
* Shared formatting helpers used across components.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* components and any future client-side
|
|
6
|
-
* calls that would explode the client bundle.
|
|
4
|
+
* Keep these dependency-free so they can be imported by both server-
|
|
5
|
+
* rendered Astro components and any future client-side scripts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -19,7 +18,7 @@ export function compact(n: number): string {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
|
-
* Format a star count compactly. Returns
|
|
21
|
+
* Format a star count compactly. Returns null when input is not a
|
|
23
22
|
* finite number, so callers can render "—" without an extra guard.
|
|
24
23
|
*/
|
|
25
24
|
export function formatStars(n: number | null | undefined): string | null {
|
|
@@ -32,9 +31,9 @@ export function formatStars(n: number | null | undefined): string | null {
|
|
|
32
31
|
* Returns "—" for missing or invalid input, and "today" for future
|
|
33
32
|
* dates (which can happen with bad upstream timestamps).
|
|
34
33
|
*/
|
|
35
|
-
export function formatRelative(
|
|
36
|
-
if (!
|
|
37
|
-
const d = new Date(
|
|
34
|
+
export function formatRelative(iso: string | null | undefined): string {
|
|
35
|
+
if (!iso) return "—";
|
|
36
|
+
const d = new Date(iso);
|
|
38
37
|
if (Number.isNaN(d.valueOf())) return "—";
|
|
39
38
|
const days = Math.floor((Date.now() - d.valueOf()) / (1000 * 60 * 60 * 24));
|
|
40
39
|
if (days <= 0) return "today";
|
|
@@ -53,10 +52,10 @@ export function formatRelative(date: string | null | undefined): string {
|
|
|
53
52
|
* Returns the original string for unparseable input, or "—" when
|
|
54
53
|
* no value is provided.
|
|
55
54
|
*/
|
|
56
|
-
export function formatDate(
|
|
57
|
-
if (!
|
|
58
|
-
const d = new Date(
|
|
59
|
-
if (Number.isNaN(d.valueOf())) return
|
|
55
|
+
export function formatDate(iso: string | null | undefined): string {
|
|
56
|
+
if (!iso) return "—";
|
|
57
|
+
const d = new Date(iso);
|
|
58
|
+
if (Number.isNaN(d.valueOf())) return iso;
|
|
60
59
|
return d.toLocaleDateString("en-US", {
|
|
61
60
|
year: "numeric",
|
|
62
61
|
month: "short",
|