@grove-dev/astro 0.2.6 → 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/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/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
|
@@ -15,6 +15,7 @@ interface StackEntry {
|
|
|
15
15
|
name: string;
|
|
16
16
|
slug: string;
|
|
17
17
|
count?: number;
|
|
18
|
+
status?: "live" | "expanding" | "planned";
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
@@ -64,6 +65,11 @@ function buildHref(slug: string): string {
|
|
|
64
65
|
{typeof s.count === "number" && (
|
|
65
66
|
<span class="font-mono text-ink-400">{s.count}</span>
|
|
66
67
|
)}
|
|
68
|
+
{s.status && s.status !== "live" && (
|
|
69
|
+
<span class="text-[10px] uppercase tracking-wide text-ink-400">
|
|
70
|
+
{s.status}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
67
73
|
</a>
|
|
68
74
|
</li>
|
|
69
75
|
))}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
import FilterOptions from "./FilterOptions.astro";
|
|
3
|
+
|
|
4
|
+
interface FacetValue {
|
|
5
|
+
value: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
label: string;
|
|
12
|
+
filterKey: string;
|
|
13
|
+
options: FacetValue[];
|
|
14
|
+
initial: string[];
|
|
15
|
+
single?: boolean;
|
|
16
|
+
active: boolean;
|
|
17
|
+
popoverId: string;
|
|
18
|
+
trigger: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const group = Astro.props;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class="grove-filter relative" data-filter-group-key={group.filterKey} data-filter-single={group.single ? "true" : "false"}>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class:list={[
|
|
28
|
+
"grove-filter-trigger inline-flex h-9 items-center gap-1.5 rounded-md border px-3 text-[0.8125rem] font-medium transition-colors",
|
|
29
|
+
group.active
|
|
30
|
+
? "border-ink-900 bg-ink-900 text-white dark:border-ink-100 dark:bg-ink-100 dark:text-ink-900"
|
|
31
|
+
: "border-ink-200 bg-white text-ink-700 hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60",
|
|
32
|
+
]}
|
|
33
|
+
aria-haspopup="listbox"
|
|
34
|
+
aria-expanded="false"
|
|
35
|
+
aria-controls={group.popoverId}
|
|
36
|
+
>
|
|
37
|
+
<span>{group.trigger}</span>
|
|
38
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="opacity-70">
|
|
39
|
+
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"/>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
<div id={group.popoverId} class="grove-filter-popover absolute left-0 top-full z-40 mt-1 hidden w-72 max-w-[calc(100vw-2rem)] rounded-lg border border-ink-200 bg-white p-1.5 shadow-lg dark:border-ink-800 dark:bg-ink-950" role="listbox" hidden>
|
|
43
|
+
<FilterOptions filterKey={group.filterKey} options={group.options} initial={group.initial} single={group.single} />
|
|
44
|
+
<div class="mt-1 flex items-center justify-between border-t border-ink-100 px-2 pt-1.5 dark:border-ink-900">
|
|
45
|
+
<button type="button" class="grove-filter-clear text-2xs font-medium text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100" hidden={!group.active}>Clear {group.label.toLowerCase()}</button>
|
|
46
|
+
<button type="button" class="grove-filter-apply ml-auto rounded-md bg-ink-900 px-2.5 py-1 text-2xs font-medium text-white dark:bg-ink-100 dark:text-ink-900">Apply</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface FacetValue {
|
|
3
|
+
value: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
count?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
filterKey: string;
|
|
10
|
+
options: FacetValue[];
|
|
11
|
+
initial: string[];
|
|
12
|
+
single?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { filterKey, options, initial, single = false } = Astro.props;
|
|
16
|
+
|
|
17
|
+
function pretty(value: string): string {
|
|
18
|
+
return value
|
|
19
|
+
.split(/[-_]/)
|
|
20
|
+
.map((part) => part ? part[0].toUpperCase() + part.slice(1) : part)
|
|
21
|
+
.join(" ");
|
|
22
|
+
}
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<ul class="m-0 flex max-h-72 list-none flex-col gap-0.5 overflow-y-auto p-0">
|
|
26
|
+
{options.map((option) => (
|
|
27
|
+
<li>
|
|
28
|
+
<label class="flex cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-[0.8125rem] text-ink-700 transition-colors hover:bg-ink-50 dark:text-ink-300 dark:hover:bg-ink-900/60">
|
|
29
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
30
|
+
<input
|
|
31
|
+
type={single ? "radio" : "checkbox"}
|
|
32
|
+
name={single ? filterKey : undefined}
|
|
33
|
+
class="grove-filter-input h-3.5 w-3.5 shrink-0 cursor-pointer accent-ink-900 dark:accent-ink-100"
|
|
34
|
+
data-filter-key={filterKey}
|
|
35
|
+
data-filter-value={option.value}
|
|
36
|
+
checked={initial.includes(option.value)}
|
|
37
|
+
/>
|
|
38
|
+
<span class="truncate">{option.label ?? pretty(option.value)}</span>
|
|
39
|
+
</span>
|
|
40
|
+
<span class="font-mono text-2xs tabular-nums text-ink-500 dark:text-ink-400">{option.count ?? 0}</span>
|
|
41
|
+
</label>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Hero — the home page banner.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Mirrors the openapps layout: trust-stats row across the top,
|
|
6
|
+
* big two-line headline with a gradient accent on the second
|
|
7
|
+
* line, descriptive paragraph, search form with a Filters
|
|
8
|
+
* shortcut, quick-filter chips, and up to 3 CTAs.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Generic: every value comes through props — site name, tagline,
|
|
14
|
-
* stats, search target. No siteConfig import.
|
|
10
|
+
* Generic: every value comes through props (or `siteConfig` for
|
|
11
|
+
* the trust stats). The label next to the count is blueprint-
|
|
12
|
+
* driven via `itemsLabel` — so a project-directory renders
|
|
13
|
+
* "apps indexed", a resource-hub renders "resources indexed",
|
|
14
|
+
* an ecosystem-map renders "entities indexed".
|
|
15
15
|
*/
|
|
16
|
+
import Container from "../layouts/Container.astro";
|
|
17
|
+
import { compact } from "../lib/format";
|
|
16
18
|
|
|
17
19
|
interface CTA {
|
|
18
20
|
label: string;
|
|
@@ -21,33 +23,43 @@ interface CTA {
|
|
|
21
23
|
external?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
interface HeroStats {
|
|
25
|
-
apps?: number;
|
|
26
|
-
categories?: number;
|
|
27
|
-
stacks?: number;
|
|
28
|
-
contributors?: number;
|
|
29
|
-
stars?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
interface Props {
|
|
33
|
-
/** Site/brand name
|
|
27
|
+
/** Site/brand name. */
|
|
34
28
|
name: string;
|
|
35
|
-
/** Short tagline
|
|
29
|
+
/** Short tagline — usually pulled from siteConfig.tagline. */
|
|
36
30
|
tagline: string;
|
|
37
|
-
/** Longer
|
|
31
|
+
/** Longer descriptive paragraph. */
|
|
38
32
|
description?: string;
|
|
39
|
-
/** Trust stats
|
|
40
|
-
stats?:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
/** Trust stats: items count, categories, stacks, contributors, stars. */
|
|
34
|
+
stats?: {
|
|
35
|
+
totalApps?: number;
|
|
36
|
+
totalCategories?: number;
|
|
37
|
+
totalStacks?: number;
|
|
38
|
+
totalOwners?: number;
|
|
39
|
+
totalStars?: number;
|
|
40
|
+
originalRepo?: string;
|
|
41
|
+
};
|
|
42
|
+
/** Search form target — the list page. */
|
|
44
43
|
searchHref?: string;
|
|
45
|
-
/**
|
|
44
|
+
/** Search input placeholder. */
|
|
45
|
+
searchPlaceholder?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Plural noun for the primary count (e.g. "apps", "resources",
|
|
48
|
+
* "entities"). Used in the trust-stats label and CTA copy.
|
|
49
|
+
*/
|
|
50
|
+
itemsLabel?: string;
|
|
51
|
+
/** Singular noun for the Submit / Browse copy. */
|
|
52
|
+
itemLabel?: string;
|
|
53
|
+
/** Quick filter chips under the search bar. */
|
|
54
|
+
quickFilters?: Array<{ label: string; href: string }>;
|
|
55
|
+
/** CTAs rendered below the form. */
|
|
46
56
|
ctas?: CTA[];
|
|
47
|
-
/** Extra class
|
|
57
|
+
/** Extra class. */
|
|
48
58
|
class?: string;
|
|
49
59
|
/** Eyebrow text rendered above the title. */
|
|
50
60
|
eyebrow?: string;
|
|
61
|
+
/** Optional badge content rendered next to the eyebrow. */
|
|
62
|
+
eyebrowBadge?: string;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
const {
|
|
@@ -55,98 +67,191 @@ const {
|
|
|
55
67
|
tagline,
|
|
56
68
|
description,
|
|
57
69
|
stats = {},
|
|
58
|
-
searchPlaceholder = "Search the directory...",
|
|
59
70
|
searchHref = "/",
|
|
71
|
+
searchPlaceholder = "Search the directory...",
|
|
72
|
+
itemsLabel = "items",
|
|
73
|
+
itemLabel,
|
|
74
|
+
quickFilters = [],
|
|
60
75
|
ctas = [],
|
|
61
76
|
class: className = "",
|
|
62
|
-
eyebrow,
|
|
77
|
+
eyebrow = "A directory of curated knowledge",
|
|
78
|
+
eyebrowBadge,
|
|
63
79
|
} = Astro.props;
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const k = n / 1000;
|
|
69
|
-
return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
|
|
70
|
-
}
|
|
71
|
-
return n.toString();
|
|
72
|
-
}
|
|
81
|
+
// Singular for the Submit-style CTA copy. Defaults to the lowercased
|
|
82
|
+
// items label so "apps" → "app" without an extra prop.
|
|
83
|
+
const singular = (itemLabel ?? itemsLabel.replace(/s$/, "")).toLowerCase();
|
|
73
84
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
stats.
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const githubUrl = stats.originalRepo;
|
|
86
|
+
|
|
87
|
+
const trustStats: { value: string; label: string }[] = [
|
|
88
|
+
...(stats.totalApps !== undefined
|
|
89
|
+
? [{ value: compact(stats.totalApps) ?? "0", label: `${itemsLabel} indexed` }]
|
|
90
|
+
: []),
|
|
91
|
+
...(stats.totalCategories !== undefined
|
|
92
|
+
? [{ value: String(stats.totalCategories), label: "categories" }]
|
|
93
|
+
: []),
|
|
94
|
+
...(stats.totalStacks !== undefined
|
|
95
|
+
? [{ value: String(stats.totalStacks), label: "stacks" }]
|
|
96
|
+
: []),
|
|
97
|
+
...(stats.totalOwners !== undefined
|
|
98
|
+
? [{ value: compact(stats.totalOwners) ?? "0", label: "contributors" }]
|
|
99
|
+
: []),
|
|
100
|
+
...(stats.totalStars !== undefined
|
|
101
|
+
? [{ value: compact(stats.totalStars) ?? "0", label: "GitHub stars" }]
|
|
102
|
+
: []),
|
|
103
|
+
];
|
|
80
104
|
---
|
|
81
105
|
|
|
82
106
|
<section
|
|
83
107
|
class:list={[
|
|
84
|
-
"
|
|
108
|
+
"relative overflow-hidden border-b border-ink-200 dark:border-ink-800",
|
|
85
109
|
className,
|
|
86
110
|
]}
|
|
87
|
-
aria-labelledby="grove-hero-heading"
|
|
88
111
|
>
|
|
89
112
|
<div
|
|
90
113
|
aria-hidden="true"
|
|
91
|
-
class="
|
|
114
|
+
class="pointer-events-none absolute inset-0 opacity-60 dark:opacity-40"
|
|
115
|
+
style="
|
|
116
|
+
background-image:
|
|
117
|
+
radial-gradient(60% 50% at 15% 20%, rgba(31, 111, 235, 0.12), transparent 60%),
|
|
118
|
+
radial-gradient(50% 40% at 85% 10%, rgba(110, 231, 183, 0.10), transparent 60%),
|
|
119
|
+
radial-gradient(40% 40% at 50% 90%, rgba(251, 146, 60, 0.08), transparent 60%);
|
|
120
|
+
"
|
|
92
121
|
></div>
|
|
93
122
|
|
|
94
|
-
<
|
|
95
|
-
<div class="
|
|
96
|
-
|
|
97
|
-
<span
|
|
123
|
+
<Container>
|
|
124
|
+
<div class="relative py-14 sm:py-20">
|
|
125
|
+
<div class="max-w-3xl">
|
|
126
|
+
<span
|
|
127
|
+
class="inline-flex animate-fade-in items-center gap-2 text-2xs font-medium text-ink-500 dark:text-ink-400"
|
|
128
|
+
>
|
|
129
|
+
<span
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"
|
|
132
|
+
></span>
|
|
98
133
|
{eyebrow}
|
|
134
|
+
{eyebrowBadge && (
|
|
135
|
+
<span
|
|
136
|
+
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-700 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
137
|
+
aria-label={eyebrowBadge}
|
|
138
|
+
>
|
|
139
|
+
{eyebrowBadge}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
99
142
|
</span>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
|
|
144
|
+
<h1
|
|
145
|
+
class="mt-3 animate-fade-in-up text-[2.25rem] sm:text-[2.75rem] md:text-[3rem] font-semibold tracking-[-0.02em] leading-[1.05] text-ink-900 dark:text-ink-100 m-0"
|
|
146
|
+
style="animation-delay: 0.05s;"
|
|
147
|
+
>
|
|
148
|
+
{name}<br class="hidden sm:block" />
|
|
149
|
+
<span
|
|
150
|
+
class="bg-gradient-to-r from-ink-900 via-ink-700 to-ink-900 bg-clip-text text-transparent dark:from-ink-100 dark:via-ink-300 dark:to-ink-100"
|
|
151
|
+
style="background-size: 200% 200%; animation: fade-in-up 0.7s ease-out 0.1s both, gradient-shift 8s ease-in-out 1s infinite;"
|
|
152
|
+
>
|
|
153
|
+
{tagline}
|
|
154
|
+
</span>
|
|
155
|
+
</h1>
|
|
156
|
+
|
|
157
|
+
{description && (
|
|
158
|
+
<p
|
|
159
|
+
class="mt-4 animate-fade-in-up text-[0.9375rem] sm:text-[1rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
|
|
160
|
+
style="animation-delay: 0.15s;"
|
|
161
|
+
>
|
|
162
|
+
{description}
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Trust stats row */}
|
|
168
|
+
{trustStats.length > 0 && (
|
|
169
|
+
<dl
|
|
170
|
+
class="mt-7 grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-4"
|
|
171
|
+
aria-label="Directory at a glance"
|
|
172
|
+
>
|
|
173
|
+
{trustStats.map((s) => (
|
|
174
|
+
<div class="flex flex-col gap-0.5">
|
|
175
|
+
<dt class="sr-only">{s.label}</dt>
|
|
176
|
+
<dd class="m-0 font-mono text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
177
|
+
{s.value}
|
|
178
|
+
</dd>
|
|
179
|
+
<dd class="m-0 text-2xs uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
180
|
+
{s.label}
|
|
181
|
+
</dd>
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
</dl>
|
|
114
185
|
)}
|
|
115
186
|
|
|
187
|
+
{/* Search + filters */}
|
|
116
188
|
<form
|
|
117
|
-
class="mt-
|
|
189
|
+
class="mt-7 flex flex-col gap-2 sm:flex-row sm:items-stretch"
|
|
118
190
|
role="search"
|
|
119
191
|
action={searchHref}
|
|
120
192
|
method="get"
|
|
121
|
-
data-grove-hero-search
|
|
122
193
|
>
|
|
123
194
|
<label class="relative flex-1">
|
|
124
|
-
<span class="sr-only">Search</span>
|
|
195
|
+
<span class="sr-only">Search {itemsLabel}, stacks, categories, licenses</span>
|
|
125
196
|
<svg
|
|
126
|
-
aria-hidden="true"
|
|
127
197
|
viewBox="0 0 16 16"
|
|
128
|
-
width="
|
|
129
|
-
height="
|
|
130
|
-
|
|
198
|
+
width="16"
|
|
199
|
+
height="16"
|
|
200
|
+
aria-hidden="true"
|
|
201
|
+
class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
131
202
|
fill="currentColor"
|
|
132
203
|
>
|
|
133
|
-
<path
|
|
204
|
+
<path
|
|
205
|
+
d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"
|
|
206
|
+
></path>
|
|
134
207
|
</svg>
|
|
135
208
|
<input
|
|
136
209
|
type="search"
|
|
137
210
|
name="q"
|
|
138
211
|
placeholder={searchPlaceholder}
|
|
139
|
-
|
|
140
|
-
class="w-full rounded-md border border-ink-200 bg-white py-2.5 pl-9 pr-3 text-sm placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
|
|
212
|
+
class="h-10 w-full rounded-md border border-ink-200 bg-white pl-10 pr-3 text-[0.9375rem] text-ink-900 placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100 dark:placeholder:text-ink-500 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
|
|
141
213
|
/>
|
|
142
214
|
</label>
|
|
143
|
-
<
|
|
215
|
+
<a
|
|
216
|
+
href={searchHref}
|
|
217
|
+
class="inline-flex h-10 items-center justify-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 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/40"
|
|
218
|
+
aria-label={`Browse all ${itemsLabel} with filters`}
|
|
219
|
+
>
|
|
220
|
+
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
|
|
221
|
+
<path d="M.75 3h14.5a.75.75 0 0 1 .55 1.26l-5.55 6.1v4.39a.75.75 0 0 1-1.2.6l-2.5-1.87a.75.75 0 0 1-.3-.6v-2.52L.2 4.26A.75.75 0 0 1 .75 3Z"/>
|
|
222
|
+
</svg>
|
|
223
|
+
<span>Filters</span>
|
|
224
|
+
</a>
|
|
225
|
+
<button
|
|
226
|
+
type="submit"
|
|
227
|
+
class="grove-btn grove-btn-primary h-10 px-4 sm:px-5"
|
|
228
|
+
>
|
|
144
229
|
Search
|
|
145
230
|
</button>
|
|
146
231
|
</form>
|
|
147
232
|
|
|
233
|
+
{quickFilters.length > 0 && (
|
|
234
|
+
<p
|
|
235
|
+
class="mt-2 text-2xs text-ink-500 dark:text-ink-400 m-0"
|
|
236
|
+
aria-hidden="true"
|
|
237
|
+
>
|
|
238
|
+
Try{" "}
|
|
239
|
+
{quickFilters.map((f, i) => (
|
|
240
|
+
<>
|
|
241
|
+
<a
|
|
242
|
+
href={f.href}
|
|
243
|
+
class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300"
|
|
244
|
+
>
|
|
245
|
+
{f.label}
|
|
246
|
+
</a>
|
|
247
|
+
{i < quickFilters.length - 1 && <span class="mx-1">·</span>}
|
|
248
|
+
</>
|
|
249
|
+
))}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
252
|
+
|
|
148
253
|
{ctas.length > 0 && (
|
|
149
|
-
<div class="mt-
|
|
254
|
+
<div class="mt-5 flex flex-wrap items-center gap-2">
|
|
150
255
|
{ctas.map((cta) => (
|
|
151
256
|
<a
|
|
152
257
|
href={cta.href}
|
|
@@ -170,77 +275,23 @@ const hasStats =
|
|
|
170
275
|
</div>
|
|
171
276
|
)}
|
|
172
277
|
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
|
|
189
|
-
{fmt(stats.categories)}
|
|
190
|
-
</dd>
|
|
191
|
-
<dt>categories</dt>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
{stats.stacks !== undefined && (
|
|
195
|
-
<div class="flex items-baseline gap-1.5">
|
|
196
|
-
<dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
|
|
197
|
-
{fmt(stats.stacks)}
|
|
198
|
-
</dd>
|
|
199
|
-
<dt>stacks</dt>
|
|
200
|
-
</div>
|
|
201
|
-
)}
|
|
202
|
-
{stats.contributors !== undefined && (
|
|
203
|
-
<div class="flex items-baseline gap-1.5">
|
|
204
|
-
<dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
|
|
205
|
-
{fmt(stats.contributors)}
|
|
206
|
-
</dd>
|
|
207
|
-
<dt>contributors</dt>
|
|
208
|
-
</div>
|
|
209
|
-
)}
|
|
210
|
-
{stats.stars !== undefined && (
|
|
211
|
-
<div class="flex items-baseline gap-1.5">
|
|
212
|
-
<dd class="m-0 font-mono text-base font-semibold text-ink-900 dark:text-ink-100">
|
|
213
|
-
{fmt(stats.stars)}
|
|
214
|
-
</dd>
|
|
215
|
-
<dt>total stars</dt>
|
|
216
|
-
</div>
|
|
217
|
-
)}
|
|
218
|
-
</dl>
|
|
278
|
+
{/* Help debug the type system when callers forget to pass ctas. */}
|
|
279
|
+
{ctas.length === 0 && githubUrl && (
|
|
280
|
+
<div class="mt-5 flex flex-wrap items-center gap-2">
|
|
281
|
+
<a href={searchHref} class="grove-btn grove-btn-primary">
|
|
282
|
+
Browse {itemsLabel}
|
|
283
|
+
</a>
|
|
284
|
+
<a
|
|
285
|
+
href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
|
|
286
|
+
target="_blank"
|
|
287
|
+
rel="noopener noreferrer"
|
|
288
|
+
class="grove-btn grove-btn-outline"
|
|
289
|
+
>
|
|
290
|
+
Submit via GitHub
|
|
291
|
+
</a>
|
|
292
|
+
</div>
|
|
219
293
|
)}
|
|
294
|
+
{singular /* suppress unused warning on the singular variable */ ? null : null}
|
|
220
295
|
</div>
|
|
221
|
-
</
|
|
296
|
+
</Container>
|
|
222
297
|
</section>
|
|
223
|
-
|
|
224
|
-
<style>
|
|
225
|
-
.grove-hero__gradient {
|
|
226
|
-
background: linear-gradient(
|
|
227
|
-
120deg,
|
|
228
|
-
rgba(31, 111, 235, 0.08) 0%,
|
|
229
|
-
rgba(110, 231, 183, 0.06) 35%,
|
|
230
|
-
rgba(253, 186, 116, 0.06) 65%,
|
|
231
|
-
rgba(147, 197, 253, 0.08) 100%
|
|
232
|
-
);
|
|
233
|
-
background-size: 200% 200%;
|
|
234
|
-
animation: var(--animate-gradient-shift, gradient-shift 8s ease-in-out infinite);
|
|
235
|
-
}
|
|
236
|
-
:global(.dark) .grove-hero__gradient {
|
|
237
|
-
background: linear-gradient(
|
|
238
|
-
120deg,
|
|
239
|
-
rgba(31, 111, 235, 0.18) 0%,
|
|
240
|
-
rgba(6, 95, 70, 0.12) 35%,
|
|
241
|
-
rgba(67, 20, 7, 0.12) 65%,
|
|
242
|
-
rgba(30, 64, 175, 0.18) 100%
|
|
243
|
-
);
|
|
244
|
-
background-size: 200% 200%;
|
|
245
|
-
}
|
|
246
|
-
</style>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* this component.
|
|
11
11
|
*
|
|
12
12
|
* The component is **generic** — it does not import any consumer-side
|
|
13
|
-
* `siteConfig`, `
|
|
13
|
+
* `siteConfig`, `item`, or blueprint-specific module. Every consumer
|
|
14
14
|
* supplies its own icon list (and optionally an `aliases` map for
|
|
15
15
|
* friendly ids) via props, and any `name` not in the registry
|
|
16
16
|
* falls back to the initials placeholder.
|
|
@@ -104,8 +104,14 @@ function resolveName(raw: string): string {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const resolved = resolveName(name);
|
|
107
|
+
const platformStackIcons = new Set(["docker"]);
|
|
108
|
+
const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
|
|
107
109
|
const folder =
|
|
108
|
-
category === "stack"
|
|
110
|
+
category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
|
|
111
|
+
? "stacks"
|
|
112
|
+
: category === "platform"
|
|
113
|
+
? "platforms"
|
|
114
|
+
: "brands";
|
|
109
115
|
const baseUrl = `${base.replace(/\/$/, "")}/${folder}/${resolved}`;
|
|
110
116
|
|
|
111
117
|
// Static: just point at the canonical svg.
|
|
@@ -116,7 +122,8 @@ const init = (fallbackInitials ?? name ?? "")
|
|
|
116
122
|
.replace(/[^a-z0-9]/gi, "")
|
|
117
123
|
.slice(0, 2)
|
|
118
124
|
.toUpperCase();
|
|
119
|
-
const showFallback =
|
|
125
|
+
const showFallback =
|
|
126
|
+
!resolved || (category === "platform" && textOnlyPlatforms.has(resolved));
|
|
120
127
|
|
|
121
128
|
// Build aria-label.
|
|
122
129
|
const ariaLabel = title ?? name;
|