@grove-dev/astro 0.3.1 → 0.3.4
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/README.md +9 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -26
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +1 -103
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +1 -28
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +1 -71
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -57
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +1 -198
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +1 -19
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +1 -30
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +1 -36
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +1 -75
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +1 -113
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +1 -379
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -10
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +1 -21
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +23 -11
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +2 -2
- package/src/components/DirectoryIndexClient.astro +240 -0
- package/src/components/FilterGroupMenu.astro +8 -4
- package/src/components/FinalCta.astro +41 -0
- package/src/components/Hero.astro +15 -18
- package/src/components/Icon.astro +35 -7
- package/src/components/IndexRow.astro +2 -2
- package/src/components/ItemCard.astro +2 -7
- package/src/components/RefinePanel.astro +7 -7
- package/src/components/SmartLensTabs.astro +9 -5
- package/src/components/StackGrid.astro +1 -1
- package/src/components/SubmissionClient.astro +133 -0
- package/src/index.ts +11 -1
- package/src/layouts/BaseLayout.astro +30 -8
- package/src/layouts/Footer.astro +25 -56
- package/src/layouts/Header.astro +33 -15
- package/src/layouts/ThemeToggle.astro +7 -3
- package/src/lib/display.ts +1 -111
- package/src/lib/format.ts +1 -64
- package/src/lib/lenses.ts +1 -220
- package/src/lib/repo.ts +1 -32
- package/src/lib/scores.ts +1 -90
- package/src/lib/search.ts +1 -418
- package/src/lib/taxonomy-counts.ts +1 -26
- package/{templates/default/src/data/records.ts → src/server/directory.ts} +12 -8
- package/src/server/index.ts +2 -0
- package/src/server/models.ts +436 -0
- package/dist/template-routes.test.d.ts +0 -2
- package/dist/template-routes.test.d.ts.map +0 -1
- package/dist/template-routes.test.js +0 -63
- package/dist/template-routes.test.js.map +0 -1
- package/dist/theme.test.d.ts +0 -2
- package/dist/theme.test.d.ts.map +0 -1
- package/dist/theme.test.js +0 -60
- package/dist/theme.test.js.map +0 -1
- package/src/template-routes.test.ts +0 -103
- package/src/theme.test.ts +0 -76
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
- package/templates/default/.github/pull_request_template.md +0 -27
- package/templates/default/.github/workflows/build.yml +0 -53
- package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -46
- package/templates/default/.github/workflows/daily-refresh.yml +0 -51
- package/templates/default/.github/workflows/sync-contributors.yml +0 -51
- package/templates/default/.github/workflows/sync-github-metadata.yml +0 -56
- package/templates/default/.github/workflows/validate-data.yml +0 -33
- package/templates/default/astro.config.mjs +0 -25
- package/templates/default/content/pages/about.md +0 -66
- package/templates/default/content/pages/methodology.md +0 -103
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/data/records/coolify.yml +0 -46
- package/templates/default/data/records/gitea.yml +0 -46
- package/templates/default/data/records/hoppscotch.yml +0 -47
- package/templates/default/data/records/inventree.yml +0 -47
- package/templates/default/data/records/label-studio.yml +0 -46
- package/templates/default/data/records/logseq.yml +0 -48
- package/templates/default/data/records/private-gpt.yml +0 -47
- package/templates/default/data/records/wakapi.yml +0 -44
- package/templates/default/data/taxonomy/categories.yml +0 -47
- package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
- package/templates/default/data/taxonomy/platforms.yml +0 -27
- package/templates/default/data/taxonomy/stacks.yml +0 -41
- package/templates/default/grove.config.ts +0 -36
- package/templates/default/package.json +0 -43
- package/templates/default/public/icons/platforms/android.svg +0 -1
- package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
- package/templates/default/public/icons/platforms/apple-light.svg +0 -1
- package/templates/default/public/icons/platforms/chrome.svg +0 -1
- package/templates/default/public/icons/platforms/chromeos.svg +0 -5
- package/templates/default/public/icons/platforms/desktop.svg +0 -4
- package/templates/default/public/icons/platforms/embedded.svg +0 -5
- package/templates/default/public/icons/platforms/ios.svg +0 -1
- package/templates/default/public/icons/platforms/linux.svg +0 -1
- package/templates/default/public/icons/platforms/macos.svg +0 -1
- package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
- package/templates/default/public/icons/platforms/web.svg +0 -5
- package/templates/default/public/icons/platforms/windows.svg +0 -6
- package/templates/default/public/icons/stacks/android.svg +0 -1
- package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
- package/templates/default/public/icons/stacks/apple-light.svg +0 -1
- package/templates/default/public/icons/stacks/bun.svg +0 -1
- package/templates/default/public/icons/stacks/capacitor.svg +0 -14
- package/templates/default/public/icons/stacks/clojure.svg +0 -1
- package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
- package/templates/default/public/icons/stacks/dart.svg +0 -1
- package/templates/default/public/icons/stacks/deno.svg +0 -1
- package/templates/default/public/icons/stacks/django.svg +0 -1
- package/templates/default/public/icons/stacks/docker.svg +0 -1
- package/templates/default/public/icons/stacks/firebase.svg +0 -1
- package/templates/default/public/icons/stacks/flutter.svg +0 -1
- package/templates/default/public/icons/stacks/go.svg +0 -1
- package/templates/default/public/icons/stacks/graphql.svg +0 -1
- package/templates/default/public/icons/stacks/ionic.svg +0 -4
- package/templates/default/public/icons/stacks/java.svg +0 -1
- package/templates/default/public/icons/stacks/javascript.svg +0 -1
- package/templates/default/public/icons/stacks/kotlin.svg +0 -1
- package/templates/default/public/icons/stacks/llm.svg +0 -1
- package/templates/default/public/icons/stacks/mongodb.svg +0 -1
- package/templates/default/public/icons/stacks/node.js.svg +0 -1
- package/templates/default/public/icons/stacks/nodejs.svg +0 -1
- package/templates/default/public/icons/stacks/python.svg +0 -1
- package/templates/default/public/icons/stacks/rag.svg +0 -7
- package/templates/default/public/icons/stacks/react-native.svg +0 -1
- package/templates/default/public/icons/stacks/react.svg +0 -1
- package/templates/default/public/icons/stacks/rust.svg +0 -1
- package/templates/default/public/icons/stacks/solidity.svg +0 -1
- package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
- package/templates/default/public/icons/stacks/swift.svg +0 -1
- package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
- package/templates/default/public/icons/stacks/typescript.svg +0 -1
- package/templates/default/public/icons/stacks/vue.svg +0 -1
- package/templates/default/public/llms-full.txt +0 -83
- package/templates/default/public/llms.txt +0 -11
- package/templates/default/public/og-image.svg +0 -48
- package/templates/default/public/robots.txt +0 -53
- package/templates/default/public/sitemap.xml +0 -63
- package/templates/default/src/pages/404.astro +0 -64
- package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -691
- package/templates/default/src/pages/[slug]/index.astro +0 -482
- package/templates/default/src/pages/about.astro +0 -189
- package/templates/default/src/pages/apps/[recordSlug].astro +0 -34
- package/templates/default/src/pages/contributors.astro +0 -175
- package/templates/default/src/pages/index.astro +0 -233
- package/templates/default/src/pages/sitemap.xml.ts +0 -89
- package/templates/default/src/pages/submit.astro +0 -281
- package/templates/default/src/styles/global.css +0 -33
- package/templates/default/tsconfig.json +0 -9
- package/templates/default/wrangler.jsonc +0 -28
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
existingSlugs: string[];
|
|
4
|
+
existingFullNames: string[];
|
|
5
|
+
repoUrl: string;
|
|
6
|
+
fields: { category: boolean; stack: boolean; platforms: boolean; tags: boolean };
|
|
7
|
+
}
|
|
8
|
+
const { existingSlugs, existingFullNames, repoUrl, fields } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<script is:inline define:vars={{ existingSlugs, existingFullNames, repoUrl, fields }}>
|
|
12
|
+
// @ts-nocheck
|
|
13
|
+
const $ = (selector) => document.querySelector(selector);
|
|
14
|
+
const inputs = {
|
|
15
|
+
repo: $("#repo-url"), name: $("#item-name"), slug: $("#item-slug"),
|
|
16
|
+
description: $("#item-description"), category: $("#item-category"),
|
|
17
|
+
stack: $("#primary-stack"), tags: $("#item-tags"),
|
|
18
|
+
website: $("#item-website"), bestFor: $("#best-for"),
|
|
19
|
+
};
|
|
20
|
+
const fetchButton = $("#fetch-repo");
|
|
21
|
+
const preview = $("#yaml-preview");
|
|
22
|
+
const status = $("#submit-status");
|
|
23
|
+
const copyButton = $("#copy-yaml");
|
|
24
|
+
const openPrLink = $("#open-pr-link");
|
|
25
|
+
const slugSet = new Set(existingSlugs);
|
|
26
|
+
const repoSet = new Set(existingFullNames);
|
|
27
|
+
let repository = null;
|
|
28
|
+
|
|
29
|
+
const slugify = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
30
|
+
const parseRepo = (value) => {
|
|
31
|
+
const match = String(value).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
32
|
+
return match ? { owner: match[1], repo: match[2].replace(/\.git$/, "") } : null;
|
|
33
|
+
};
|
|
34
|
+
const quote = (value) => `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
35
|
+
const lines = (values, indent = 2) => values.map((value) => `${" ".repeat(indent)}- ${value}`).join("\n");
|
|
36
|
+
const selectedPlatforms = () => [...document.querySelectorAll("input[name=platform]:checked")].map((input) => input.value);
|
|
37
|
+
const setStatus = (text, tone = "muted") => {
|
|
38
|
+
status.textContent = text;
|
|
39
|
+
status.className = `mt-3 min-h-5 text-2xs ${tone === "error" ? "text-red-600 dark:text-red-400" : tone === "warn" ? "text-amber-700 dark:text-amber-400" : tone === "success" ? "text-emerald-700 dark:text-emerald-400" : "text-ink-500 dark:text-ink-400"}`;
|
|
40
|
+
};
|
|
41
|
+
const inferStack = (repo) => {
|
|
42
|
+
const language = String(repo.language || "").toLowerCase();
|
|
43
|
+
const topics = (repo.topics || []).map((topic) => String(topic).toLowerCase());
|
|
44
|
+
if (topics.includes("flutter") || language === "dart") return "flutter";
|
|
45
|
+
if (topics.includes("react-native")) return "react-native";
|
|
46
|
+
if (language === "swift" || language === "objective-c") return "ios";
|
|
47
|
+
if (language === "kotlin" || language === "java") return "android";
|
|
48
|
+
return language;
|
|
49
|
+
};
|
|
50
|
+
const generateYaml = () => {
|
|
51
|
+
if (!repository) return "";
|
|
52
|
+
const parsed = parseRepo(inputs.repo.value);
|
|
53
|
+
if (!parsed) return "";
|
|
54
|
+
const tags = String(inputs.tags?.value || "").split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
55
|
+
const platforms = selectedPlatforms();
|
|
56
|
+
const bestFor = inputs.bestFor.value.split("\n").map((item) => item.trim()).filter(Boolean);
|
|
57
|
+
return [
|
|
58
|
+
"kind: project",
|
|
59
|
+
`slug: ${slugify(inputs.slug.value || repository.name)}`,
|
|
60
|
+
`name: ${quote(inputs.name.value)}`,
|
|
61
|
+
`description: ${quote(inputs.description.value)}`,
|
|
62
|
+
...(fields.category ? [`category: ${inputs.category?.value || "uncategorized"}`] : []),
|
|
63
|
+
"projectType: real-app",
|
|
64
|
+
...(fields.stack && inputs.stack?.value ? [`stack: ${quote(inputs.stack.value)}`] : []),
|
|
65
|
+
...(fields.platforms ? ["platforms:", platforms.length ? lines(platforms) : " []"] : []),
|
|
66
|
+
...(fields.tags ? ["tags:", tags.length ? lines(tags) : " []"] : []),
|
|
67
|
+
`repoUrl: ${repository.html_url}`,
|
|
68
|
+
"links:",
|
|
69
|
+
` github: ${repository.html_url}`,
|
|
70
|
+
...(inputs.website.value ? [` website: ${inputs.website.value}`] : []),
|
|
71
|
+
"bestFor:", bestFor.length ? lines(bestFor) : " []",
|
|
72
|
+
"source:",
|
|
73
|
+
" type: manual",
|
|
74
|
+
` owner: ${parsed.owner}`,
|
|
75
|
+
` repo: ${parsed.repo}`,
|
|
76
|
+
"curation:",
|
|
77
|
+
" reviewed: false",
|
|
78
|
+
" labels: []",
|
|
79
|
+
" lenses: []",
|
|
80
|
+
"",
|
|
81
|
+
].join("\n");
|
|
82
|
+
};
|
|
83
|
+
const refresh = () => {
|
|
84
|
+
const yaml = generateYaml();
|
|
85
|
+
preview.textContent = yaml || "Paste a GitHub URL to generate a draft.";
|
|
86
|
+
if (!yaml) return;
|
|
87
|
+
const params = new URLSearchParams({ filename: `data/records/${slugify(inputs.slug.value)}.yml`, value: yaml });
|
|
88
|
+
openPrLink.href = `${repoUrl}/new/main?${params}`;
|
|
89
|
+
openPrLink.classList.remove("pointer-events-none", "opacity-50");
|
|
90
|
+
copyButton.disabled = false;
|
|
91
|
+
copyButton.classList.remove("pointer-events-none", "opacity-50");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
fetchButton.addEventListener("click", async () => {
|
|
95
|
+
const parsed = parseRepo(inputs.repo.value);
|
|
96
|
+
if (!parsed) return setStatus("Enter a valid GitHub repository URL.", "error");
|
|
97
|
+
const fullName = `${parsed.owner}/${parsed.repo}`.toLowerCase();
|
|
98
|
+
if (repoSet.has(fullName)) setStatus("This repository is already listed. The draft can update its record.", "warn");
|
|
99
|
+
fetchButton.disabled = true;
|
|
100
|
+
setStatus("Fetching GitHub metadata...");
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, { headers: { Accept: "application/vnd.github+json" } });
|
|
103
|
+
if (response.status === 404) throw new Error("Repository not found or not public.");
|
|
104
|
+
if (response.status === 403) throw new Error("GitHub rate limit reached. Try again shortly.");
|
|
105
|
+
if (!response.ok) throw new Error(`GitHub returned ${response.status}.`);
|
|
106
|
+
repository = await response.json();
|
|
107
|
+
if (repository.private) throw new Error("Private repositories cannot be submitted.");
|
|
108
|
+
inputs.name.value = repository.name || parsed.repo;
|
|
109
|
+
inputs.slug.value = slugify(repository.name || parsed.repo);
|
|
110
|
+
inputs.description.value = repository.description || "";
|
|
111
|
+
if (inputs.stack) inputs.stack.value = inferStack(repository);
|
|
112
|
+
if (inputs.tags) inputs.tags.value = (repository.topics || []).slice(0, 8).join(", ");
|
|
113
|
+
inputs.website.value = repository.homepage || "";
|
|
114
|
+
const duplicate = slugSet.has(inputs.slug.value);
|
|
115
|
+
setStatus(duplicate ? "That slug already exists. Choose another or update the existing record." : "Draft generated. Edit any field and the YAML will refresh.", duplicate ? "warn" : "success");
|
|
116
|
+
refresh();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
repository = null;
|
|
119
|
+
setStatus(error.message || "Unable to fetch repository metadata.", "error");
|
|
120
|
+
} finally {
|
|
121
|
+
fetchButton.disabled = false;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
$("#submit-form").addEventListener("input", refresh);
|
|
125
|
+
copyButton.addEventListener("click", async () => {
|
|
126
|
+
try {
|
|
127
|
+
await navigator.clipboard.writeText(preview.textContent || "");
|
|
128
|
+
setStatus("Copied YAML to clipboard.", "success");
|
|
129
|
+
} catch {
|
|
130
|
+
setStatus("Couldn't copy automatically. Select the YAML and copy it.", "error");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
</script>
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { existsSync } from "node:fs";
|
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
29
|
import { dirname, resolve } from "node:path";
|
|
30
30
|
import type { AstroIntegration } from "astro";
|
|
31
|
+
import { loadConfig, prepareDirectory } from "@grove-dev/core";
|
|
31
32
|
|
|
32
33
|
export * from "@grove-dev/core";
|
|
33
34
|
|
|
@@ -57,13 +58,21 @@ export default function groveAstro(): AstroIntegration {
|
|
|
57
58
|
return {
|
|
58
59
|
name: "@grove-dev/astro",
|
|
59
60
|
hooks: {
|
|
60
|
-
"astro:config:setup": ({ config, updateConfig, injectScript }) => {
|
|
61
|
+
"astro:config:setup": async ({ config, updateConfig, injectScript }) => {
|
|
61
62
|
// Alias the components/layouts source directories so
|
|
62
63
|
// consumer builds can import from
|
|
63
64
|
// @grove-dev/astro/components/ItemCard.astro
|
|
64
65
|
// without us shipping a glob-shaped `exports` map that
|
|
65
66
|
// Vite/Rollup does not expand reliably.
|
|
66
67
|
const consumerRoot = fileURLToPath(config.root);
|
|
68
|
+
const groveConfigPath = resolve(consumerRoot, "grove.config.ts");
|
|
69
|
+
if (existsSync(groveConfigPath)) {
|
|
70
|
+
await prepareDirectory(consumerRoot);
|
|
71
|
+
// Preparation is the integration's only site-level side effect.
|
|
72
|
+
// Routes belong to the consumer's `src/pages`, where they remain
|
|
73
|
+
// visible, editable, and safe from future Grove syncs.
|
|
74
|
+
await loadConfig(consumerRoot);
|
|
75
|
+
}
|
|
67
76
|
const globalCssPath = resolve(consumerRoot, "src/styles/global.css");
|
|
68
77
|
const globalCssExists = existsSync(globalCssPath);
|
|
69
78
|
|
|
@@ -73,6 +82,7 @@ export default function groveAstro(): AstroIntegration {
|
|
|
73
82
|
alias: {
|
|
74
83
|
"@grove-dev/astro/components": componentsDir,
|
|
75
84
|
"@grove-dev/astro/layouts": layoutsDir,
|
|
85
|
+
"@grove/generated": resolve(consumerRoot, "data/generated"),
|
|
76
86
|
},
|
|
77
87
|
},
|
|
78
88
|
plugins: [
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* own config (typically `data/generated/site-config.json` for the
|
|
12
12
|
* showcase).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* privacy-first deployments.
|
|
14
|
+
* GA4 can be configured once through `site.analytics.googleAnalyticsId`
|
|
15
|
+
* or overridden per page with `gaId`. Omit both for privacy-first deployments.
|
|
16
16
|
*/
|
|
17
17
|
import "../styles.css";
|
|
18
18
|
import Header from "./Header.astro";
|
|
@@ -30,6 +30,9 @@ interface SiteStats {
|
|
|
30
30
|
totalStacks?: number;
|
|
31
31
|
totalOwners?: number;
|
|
32
32
|
totalStars?: number;
|
|
33
|
+
repositoryStars?: number;
|
|
34
|
+
repositoryForks?: number;
|
|
35
|
+
repositoryContributors?: number;
|
|
33
36
|
originalRepo?: string;
|
|
34
37
|
originalStars?: number;
|
|
35
38
|
originalForks?: number;
|
|
@@ -41,8 +44,26 @@ interface Site {
|
|
|
41
44
|
tagline?: string;
|
|
42
45
|
description?: string;
|
|
43
46
|
nav: NavItem[];
|
|
47
|
+
footer?: {
|
|
48
|
+
columns?: Array<{
|
|
49
|
+
heading: string;
|
|
50
|
+
items: Array<{ label: string; href: string; external?: boolean }>;
|
|
51
|
+
}>;
|
|
52
|
+
copyright?: string;
|
|
53
|
+
license?: string;
|
|
54
|
+
};
|
|
55
|
+
submission?: {
|
|
56
|
+
eyebrow?: string;
|
|
57
|
+
title?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
good?: string[];
|
|
60
|
+
avoid?: string[];
|
|
61
|
+
};
|
|
44
62
|
repoUrl?: string;
|
|
45
63
|
url?: string;
|
|
64
|
+
analytics?: {
|
|
65
|
+
googleAnalyticsId?: string;
|
|
66
|
+
};
|
|
46
67
|
/** Optional directory-level stats — populated from site-config.json
|
|
47
68
|
* at generate time. Components use it to render hero counts,
|
|
48
69
|
* origin card stats, and footer summary lines. */
|
|
@@ -85,6 +106,7 @@ const {
|
|
|
85
106
|
} = Astro.props;
|
|
86
107
|
const directoryPath =
|
|
87
108
|
searchPath ?? `/${site.blueprintConfig?.routeSlug ?? "items"}`;
|
|
109
|
+
const effectiveGaId = gaId ?? site.analytics?.googleAnalyticsId;
|
|
88
110
|
|
|
89
111
|
const favicon =
|
|
90
112
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2318181b'/%3E%3C/svg%3E";
|
|
@@ -112,14 +134,14 @@ const favicon =
|
|
|
112
134
|
title="LLM usage guidance"
|
|
113
135
|
/>
|
|
114
136
|
|
|
115
|
-
{
|
|
137
|
+
{effectiveGaId && (
|
|
116
138
|
<>
|
|
117
139
|
<script
|
|
118
140
|
is:inline
|
|
119
141
|
async
|
|
120
|
-
src={`https://www.googletagmanager.com/gtag/js?id=${
|
|
142
|
+
src={`https://www.googletagmanager.com/gtag/js?id=${effectiveGaId}`}
|
|
121
143
|
/>
|
|
122
|
-
<script is:inline define:vars={{ gaId }}>
|
|
144
|
+
<script is:inline define:vars={{ gaId: effectiveGaId }}>
|
|
123
145
|
window.dataLayer = window.dataLayer || [];
|
|
124
146
|
function gtag() {
|
|
125
147
|
dataLayer.push(arguments);
|
|
@@ -215,7 +237,7 @@ const favicon =
|
|
|
215
237
|
...(site.stats?.totalApps
|
|
216
238
|
? [{ label: "Browse all", href: directoryPath }]
|
|
217
239
|
: []),
|
|
218
|
-
{ label: "
|
|
240
|
+
{ label: "Community", href: "/contributors" },
|
|
219
241
|
...(site.stats?.originalRepo
|
|
220
242
|
? [{ label: "View on GitHub", href: site.stats.originalRepo, external: true }]
|
|
221
243
|
: []),
|
|
@@ -272,8 +294,8 @@ const favicon =
|
|
|
272
294
|
|
|
273
295
|
<slot name="body-end" />
|
|
274
296
|
|
|
275
|
-
{
|
|
276
|
-
<script is:inline define:vars={{ gaId }}>
|
|
297
|
+
{effectiveGaId && (
|
|
298
|
+
<script is:inline define:vars={{ gaId: effectiveGaId }}>
|
|
277
299
|
(function () {
|
|
278
300
|
// GA4's `gtag('config', id)` call in the <head> already auto-pings
|
|
279
301
|
// `page_view` on full navigations, so this block only needs to
|
package/src/layouts/Footer.astro
CHANGED
|
@@ -30,6 +30,11 @@ interface Site {
|
|
|
30
30
|
tagline?: string;
|
|
31
31
|
description?: string;
|
|
32
32
|
repoUrl?: string;
|
|
33
|
+
footer?: {
|
|
34
|
+
columns?: FooterColumn[];
|
|
35
|
+
copyright?: string;
|
|
36
|
+
license?: string;
|
|
37
|
+
};
|
|
33
38
|
stats?: {
|
|
34
39
|
totalApps?: number;
|
|
35
40
|
totalCategories?: number;
|
|
@@ -74,8 +79,9 @@ const {
|
|
|
74
79
|
} = Astro.props;
|
|
75
80
|
|
|
76
81
|
const year = new Date().getFullYear();
|
|
77
|
-
const holder = copyrightHolder ?? site.name;
|
|
82
|
+
const holder = copyrightHolder ?? site.footer?.copyright ?? site.name;
|
|
78
83
|
const gh = site.repoUrl;
|
|
84
|
+
const showOwners = (stats?.totalOwners ?? 0) > 0;
|
|
79
85
|
|
|
80
86
|
const defaultDiscover: NavItem[] = gh
|
|
81
87
|
? [
|
|
@@ -100,6 +106,15 @@ const defaultProject: NavItem[] = gh
|
|
|
100
106
|
const discoverItems = discover.items.length ? discover.items : defaultDiscover;
|
|
101
107
|
const contributeItems = contribute.items.length ? contribute.items : defaultContribute;
|
|
102
108
|
const projectItems = project.items.length ? project.items : defaultProject;
|
|
109
|
+
const configuredColumns = site.footer?.columns ?? [];
|
|
110
|
+
const columns = configuredColumns.length
|
|
111
|
+
? configuredColumns
|
|
112
|
+
: [
|
|
113
|
+
{ heading: discover.heading, items: discoverItems },
|
|
114
|
+
{ heading: contribute.heading, items: contributeItems },
|
|
115
|
+
{ heading: project.heading, items: projectItems },
|
|
116
|
+
].filter((column) => column.items.length > 0);
|
|
117
|
+
const resolvedLicenseLine = licenseLine ?? site.footer?.license;
|
|
103
118
|
---
|
|
104
119
|
|
|
105
120
|
<footer
|
|
@@ -133,7 +148,7 @@ const projectItems = project.items.length ? project.items : defaultProject;
|
|
|
133
148
|
<span class="font-mono">{stats.totalApps}</span> {itemsLabel}
|
|
134
149
|
</>
|
|
135
150
|
)}
|
|
136
|
-
{stats.totalApps !== undefined && (stats.totalCategories !== undefined ||
|
|
151
|
+
{stats.totalApps !== undefined && (stats.totalCategories !== undefined || showOwners) && (
|
|
137
152
|
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
138
153
|
)}
|
|
139
154
|
{stats.totalCategories !== undefined && (
|
|
@@ -141,71 +156,25 @@ const projectItems = project.items.length ? project.items : defaultProject;
|
|
|
141
156
|
<span class="font-mono">{stats.totalCategories}</span> categories
|
|
142
157
|
</>
|
|
143
158
|
)}
|
|
144
|
-
{stats.totalCategories !== undefined &&
|
|
159
|
+
{stats.totalCategories !== undefined && showOwners && (
|
|
145
160
|
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
146
161
|
)}
|
|
147
|
-
{
|
|
162
|
+
{showOwners && (
|
|
148
163
|
<>
|
|
149
|
-
<span class="font-mono">{stats.totalOwners}</span>
|
|
164
|
+
<span class="font-mono">{stats.totalOwners}</span> project owners
|
|
150
165
|
</>
|
|
151
166
|
)}
|
|
152
167
|
</p>
|
|
153
168
|
)}
|
|
154
169
|
</div>
|
|
155
170
|
|
|
156
|
-
{
|
|
157
|
-
<div>
|
|
158
|
-
<h4 class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100">
|
|
159
|
-
{discover.heading}
|
|
160
|
-
</h4>
|
|
161
|
-
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
162
|
-
{discoverItems.map((item) => (
|
|
163
|
-
<li>
|
|
164
|
-
<a
|
|
165
|
-
href={item.href}
|
|
166
|
-
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
167
|
-
{...(item.external
|
|
168
|
-
? { target: "_blank", rel: "noopener noreferrer" }
|
|
169
|
-
: {})}
|
|
170
|
-
>
|
|
171
|
-
{item.label}
|
|
172
|
-
</a>
|
|
173
|
-
</li>
|
|
174
|
-
))}
|
|
175
|
-
</ul>
|
|
176
|
-
</div>
|
|
177
|
-
)}
|
|
178
|
-
|
|
179
|
-
{contributeItems.length > 0 && (
|
|
180
|
-
<div>
|
|
181
|
-
<h4 class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100">
|
|
182
|
-
{contribute.heading}
|
|
183
|
-
</h4>
|
|
184
|
-
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
185
|
-
{contributeItems.map((item) => (
|
|
186
|
-
<li>
|
|
187
|
-
<a
|
|
188
|
-
href={item.href}
|
|
189
|
-
class="text-sm transition-colors hover:text-ink-900 dark:hover:text-ink-100"
|
|
190
|
-
{...(item.external
|
|
191
|
-
? { target: "_blank", rel: "noopener noreferrer" }
|
|
192
|
-
: {})}
|
|
193
|
-
>
|
|
194
|
-
{item.label}
|
|
195
|
-
</a>
|
|
196
|
-
</li>
|
|
197
|
-
))}
|
|
198
|
-
</ul>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
|
|
202
|
-
{projectItems.length > 0 && (
|
|
171
|
+
{columns.map((column) => (
|
|
203
172
|
<div>
|
|
204
173
|
<h4 class="mb-3 text-2xs font-semibold uppercase tracking-wider text-ink-900 dark:text-ink-100">
|
|
205
|
-
{
|
|
174
|
+
{column.heading}
|
|
206
175
|
</h4>
|
|
207
176
|
<ul class="m-0 flex list-none flex-col gap-2 p-0">
|
|
208
|
-
{
|
|
177
|
+
{column.items.map((item) => (
|
|
209
178
|
<li>
|
|
210
179
|
<a
|
|
211
180
|
href={item.href}
|
|
@@ -220,14 +189,14 @@ const projectItems = project.items.length ? project.items : defaultProject;
|
|
|
220
189
|
))}
|
|
221
190
|
</ul>
|
|
222
191
|
</div>
|
|
223
|
-
)}
|
|
192
|
+
))}
|
|
224
193
|
</div>
|
|
225
194
|
|
|
226
195
|
<div class="border-t border-ink-200 dark:border-ink-800">
|
|
227
196
|
<div
|
|
228
197
|
class="mx-auto flex w-full max-w-wide flex-wrap items-center gap-3 px-5 py-4 text-2xs text-ink-500 sm:px-7 dark:text-ink-400"
|
|
229
198
|
>
|
|
230
|
-
<span>© {year} {holder}{
|
|
199
|
+
<span>© {year} {holder}{resolvedLicenseLine ? `. ${resolvedLicenseLine}` : "."}</span>
|
|
231
200
|
{gh && (
|
|
232
201
|
<>
|
|
233
202
|
<span class="text-ink-300 dark:text-ink-700">·</span>
|
package/src/layouts/Header.astro
CHANGED
|
@@ -28,7 +28,8 @@ interface Site {
|
|
|
28
28
|
repoUrl?: string;
|
|
29
29
|
url?: string;
|
|
30
30
|
stats?: {
|
|
31
|
-
|
|
31
|
+
/** Stars on this site's own source repository. */
|
|
32
|
+
repositoryStars?: number;
|
|
32
33
|
};
|
|
33
34
|
blueprintConfig?: {
|
|
34
35
|
routeSlug?: string;
|
|
@@ -55,20 +56,21 @@ const {
|
|
|
55
56
|
class: className = "",
|
|
56
57
|
} = Astro.props;
|
|
57
58
|
const directoryPath = `/${site.blueprintConfig?.routeSlug ?? "items"}`;
|
|
58
|
-
const
|
|
59
|
+
const defaultNavItems = site.blueprintConfig
|
|
59
60
|
? [
|
|
60
|
-
{ label: "Recently added", href: `${directoryPath}?
|
|
61
|
+
{ label: "Recently added", href: `${directoryPath}?sort=recently-added` },
|
|
61
62
|
{ label: "Trending", href: `${directoryPath}?label=hot` },
|
|
62
63
|
{ label: "Categories", href: directoryPath },
|
|
63
64
|
{ label: "Stacks", href: directoryPath },
|
|
64
65
|
{ label: "About", href: "/about" },
|
|
65
66
|
]
|
|
66
|
-
:
|
|
67
|
-
const
|
|
67
|
+
: [];
|
|
68
|
+
const navItems = site.nav?.length ? site.nav : defaultNavItems;
|
|
69
|
+
const stars = site.stats?.repositoryStars;
|
|
68
70
|
const starsLabel =
|
|
69
|
-
stars >= 1000
|
|
71
|
+
typeof stars === "number" && stars >= 1000
|
|
70
72
|
? `${(stars / 1000).toFixed(stars >= 10000 ? 0 : 1)}k`
|
|
71
|
-
: String(stars);
|
|
73
|
+
: typeof stars === "number" ? String(stars) : null;
|
|
72
74
|
---
|
|
73
75
|
|
|
74
76
|
<header
|
|
@@ -78,17 +80,17 @@ const starsLabel =
|
|
|
78
80
|
]}
|
|
79
81
|
>
|
|
80
82
|
<div
|
|
81
|
-
class="mx-auto flex w-full max-w-wide items-center gap-
|
|
83
|
+
class="mx-auto flex w-full max-w-wide items-center gap-3 px-5 py-3 sm:gap-6 sm:px-7"
|
|
82
84
|
>
|
|
83
85
|
<a
|
|
84
86
|
href="/"
|
|
85
|
-
class="inline-flex items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
87
|
+
class="inline-flex min-w-0 shrink items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
86
88
|
>
|
|
87
89
|
<span
|
|
88
90
|
aria-hidden="true"
|
|
89
91
|
class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
|
|
90
92
|
></span>
|
|
91
|
-
<span>{site.name}</span>
|
|
93
|
+
<span class="truncate">{site.name}</span>
|
|
92
94
|
</a>
|
|
93
95
|
|
|
94
96
|
<nav class="hidden flex-1 md:block" aria-label="Primary">
|
|
@@ -107,10 +109,26 @@ const starsLabel =
|
|
|
107
109
|
</nav>
|
|
108
110
|
|
|
109
111
|
<div class="ml-auto flex items-center gap-2">
|
|
112
|
+
<details class="group relative md:hidden">
|
|
113
|
+
<summary class="inline-flex h-9 cursor-pointer list-none items-center justify-center rounded-[var(--radius)] border border-border bg-secondary px-2.5 text-sm font-medium text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/35">
|
|
114
|
+
Menu
|
|
115
|
+
</summary>
|
|
116
|
+
<nav aria-label="Mobile primary" class="absolute right-0 top-[calc(100%+0.5rem)] z-50 w-56 rounded-[var(--radius)] border border-border bg-background p-2 shadow-lg">
|
|
117
|
+
<ul class="m-0 flex list-none flex-col gap-1 p-0">
|
|
118
|
+
{navItems.map((item) => (
|
|
119
|
+
<li><a href={item.href} class="block rounded-md px-3 py-2 text-sm font-medium text-foreground no-underline hover:bg-accent">{item.label}</a></li>
|
|
120
|
+
))}
|
|
121
|
+
{submitHref && (
|
|
122
|
+
<li class="border-t border-border pt-1"><a href={submitHref} class="block rounded-md px-3 py-2 text-sm font-medium text-foreground no-underline hover:bg-accent">{submitLabel ?? "Submit"}</a></li>
|
|
123
|
+
)}
|
|
124
|
+
</ul>
|
|
125
|
+
</nav>
|
|
126
|
+
</details>
|
|
127
|
+
|
|
110
128
|
{submitHref && (
|
|
111
129
|
<a
|
|
112
130
|
href={submitHref}
|
|
113
|
-
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 border-border bg-secondary text-foreground hover:bg-accent hidden
|
|
131
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 border-border bg-secondary text-foreground hover:bg-accent max-sm:!hidden"
|
|
114
132
|
>
|
|
115
133
|
<svg
|
|
116
134
|
viewBox="0 0 16 16"
|
|
@@ -130,7 +148,7 @@ const starsLabel =
|
|
|
130
148
|
href={site.repoUrl}
|
|
131
149
|
target="_blank"
|
|
132
150
|
rel="noopener noreferrer"
|
|
133
|
-
aria-label={`${repoLabel} on GitHub — ${stars} stars`}
|
|
151
|
+
aria-label={`${repoLabel} on GitHub${starsLabel ? ` — ${stars} stars` : ""}`}
|
|
134
152
|
title={`${repoLabel} on GitHub`}
|
|
135
153
|
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-[var(--radius)] border border-border bg-secondary p-0 text-sm font-medium leading-none text-foreground no-underline outline-none transition-colors hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"
|
|
136
154
|
>
|
|
@@ -138,14 +156,14 @@ const starsLabel =
|
|
|
138
156
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="currentColor">
|
|
139
157
|
<path 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"></path>
|
|
140
158
|
</svg>
|
|
141
|
-
<span>{repoLabel}</span>
|
|
159
|
+
<span class="hidden lg:inline">{repoLabel}</span>
|
|
142
160
|
</span>
|
|
143
|
-
<span class="
|
|
161
|
+
{starsLabel && <span class="hidden items-center gap-1 border-s border-border bg-secondary px-2 py-1.5 text-secondary-foreground sm:flex">
|
|
144
162
|
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
145
163
|
<path 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"></path>
|
|
146
164
|
</svg>
|
|
147
165
|
<span class="font-mono">{starsLabel}</span>
|
|
148
|
-
</span>
|
|
166
|
+
</span>}
|
|
149
167
|
</a>
|
|
150
168
|
)}
|
|
151
169
|
|
|
@@ -24,11 +24,13 @@ const { class: className = "" } = Astro.props;
|
|
|
24
24
|
data-theme-toggle
|
|
25
25
|
aria-label="Toggle theme"
|
|
26
26
|
title="Toggle theme"
|
|
27
|
-
class={`inline-flex h-
|
|
27
|
+
class={`inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-[var(--radius)] border border-border bg-secondary p-0 text-foreground outline-none transition-colors hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 ${className}`}
|
|
28
28
|
>
|
|
29
29
|
{/* Sun — shown in dark mode (click to go light) */}
|
|
30
30
|
<svg
|
|
31
|
-
|
|
31
|
+
width="18"
|
|
32
|
+
height="18"
|
|
33
|
+
class="hidden shrink-0 dark:block"
|
|
32
34
|
viewBox="0 0 24 24"
|
|
33
35
|
fill="none"
|
|
34
36
|
stroke="currentColor"
|
|
@@ -42,7 +44,9 @@ const { class: className = "" } = Astro.props;
|
|
|
42
44
|
</svg>
|
|
43
45
|
{/* Moon — shown in light mode (click to go dark) */}
|
|
44
46
|
<svg
|
|
45
|
-
|
|
47
|
+
width="18"
|
|
48
|
+
height="18"
|
|
49
|
+
class="block shrink-0 dark:hidden"
|
|
46
50
|
viewBox="0 0 24 24"
|
|
47
51
|
fill="none"
|
|
48
52
|
stroke="currentColor"
|
package/src/lib/display.ts
CHANGED
|
@@ -1,111 +1 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
7
|
-
* Every export here is dependency-free so it can be imported by both
|
|
8
|
-
* server-rendered Astro components and any client-side script.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// ── Label ids (the `labels` array on an item) ────────────────────────
|
|
12
|
-
export type LabelId = "new" | "hot" | "mature" | "featured";
|
|
13
|
-
|
|
14
|
-
export const LABEL_DISPLAY: Record<LabelId, string> = {
|
|
15
|
-
new: "Recently added",
|
|
16
|
-
hot: "Trending",
|
|
17
|
-
mature: "Established",
|
|
18
|
-
featured: "Featured",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/** Returns a human-readable name for a label id, or null if unknown. */
|
|
22
|
-
export function labelDisplay(id: string | null | undefined): string | null {
|
|
23
|
-
if (!id) return null;
|
|
24
|
-
return LABEL_DISPLAY[id as LabelId] ?? null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── Status ids (the `status` field on an item) ───────────────────────
|
|
28
|
-
/**
|
|
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).
|
|
32
|
-
*/
|
|
33
|
-
export const STATUS_DISPLAY: Record<string, string> = {
|
|
34
|
-
active: "Active",
|
|
35
|
-
mature: "Mature",
|
|
36
|
-
quiet: "Quiet",
|
|
37
|
-
stale: "Not recently active",
|
|
38
|
-
inactive: "Inactive",
|
|
39
|
-
"needs-maintainer": "Needs maintainer",
|
|
40
|
-
archived: "Archived",
|
|
41
|
-
historical: "Historical",
|
|
42
|
-
needs_review: "Needs review",
|
|
43
|
-
unavailable: "Unavailable",
|
|
44
|
-
unknown: "Unknown",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export function statusDisplay(s: string | null | undefined): string {
|
|
48
|
-
if (!s) return STATUS_DISPLAY.unknown ?? "";
|
|
49
|
-
return STATUS_DISPLAY[s] ?? STATUS_DISPLAY.unknown ?? "";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Lens ids (the curated tabs on /items) ────────────────────────────
|
|
53
|
-
export const LENS_DISPLAY: Record<string, string> = {
|
|
54
|
-
all: "All items",
|
|
55
|
-
new: "Recently added",
|
|
56
|
-
hot: "Trending",
|
|
57
|
-
mature: "Established",
|
|
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",
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export function lensDisplay(id: string | null | undefined): string {
|
|
68
|
-
if (!id) return LENS_DISPLAY.all ?? "All";
|
|
69
|
-
return LENS_DISPLAY[id] ?? id;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Sort ids (the /items sort dropdown) ──────────────────────────────
|
|
73
|
-
export const SORT_DISPLAY: Record<string, string> = {
|
|
74
|
-
"recently-updated": "Recently updated",
|
|
75
|
-
"most-starred": "Most starred",
|
|
76
|
-
"recently-added": "Recently added",
|
|
77
|
-
"best-overall": "Best overall",
|
|
78
|
-
alphabetical: "Alphabetical",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export function sortDisplay(id: string | null | undefined): string {
|
|
82
|
-
if (!id) return SORT_DISPLAY["recently-updated"] ?? "";
|
|
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(" ");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ── Status option list (used by filter dropdowns) ───────────────────
|
|
96
|
-
// "needs-maintainer" is included even though it's a derived/composite
|
|
97
|
-
// status — filter dropdowns and chip rendering need to offer it
|
|
98
|
-
// alongside the raw AppStatus values.
|
|
99
|
-
export const STATUS_OPTIONS: string[] = [
|
|
100
|
-
"active",
|
|
101
|
-
"mature",
|
|
102
|
-
"quiet",
|
|
103
|
-
"stale",
|
|
104
|
-
"inactive",
|
|
105
|
-
"needs-maintainer",
|
|
106
|
-
"archived",
|
|
107
|
-
"historical",
|
|
108
|
-
"needs_review",
|
|
109
|
-
"unavailable",
|
|
110
|
-
"unknown",
|
|
111
|
-
];
|
|
1
|
+
export * from "@grove-dev/core";
|