@grove-dev/astro 0.2.10 → 0.2.14
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 +26 -18
- 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 +12 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +13 -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 +44 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +192 -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/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/IndexRow.astro +75 -0
- package/src/components/ItemCard.astro +266 -248
- package/src/components/Pagination.astro +63 -0
- package/src/components/RecordSection.astro +70 -34
- 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 +17 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +218 -291
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +209 -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 +138 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[recordSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +477 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[recordSlug].astro +33 -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/src/components/AppsIndexRow.astro +0 -204
- package/src/components/AppsPagination.astro +0 -126
- 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,23 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* RecordSection —
|
|
3
|
+
* RecordSection — generic lens-style section wrapper for the
|
|
4
|
+
* home page.
|
|
4
5
|
*
|
|
5
|
-
* Renders a SectionHeader + a 3-column grid of `ItemCard`s
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Renders a SectionHeader + a 3-column grid of `ItemCard`s (a
|
|
7
|
+
* record card; kept as `ItemCard` for V1 published API stability),
|
|
8
|
+
* optional "View all" link in the header. Generic: takes any
|
|
9
|
+
* `IndexRecord[]` (the slim list-page shape, discriminated union
|
|
10
|
+
* over `kind`).
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
+
* Two equivalent APIs are supported so the same template works
|
|
13
|
+
* against both the workspace source and the published
|
|
14
|
+
* `@grove-dev/astro@0.2.10` package:
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* - **`hrefFor(record)`** callback — V1 published API. Caller
|
|
17
|
+
* passes a function that returns the detail-page URL for
|
|
18
|
+
* each item.
|
|
19
|
+
* - **`pathPrefix` string** — convenience API for the common
|
|
20
|
+
* case `${pathPrefix}/${item.slug}`. Mutually exclusive with
|
|
21
|
+
* `hrefFor`; if both are provided, `hrefFor` wins.
|
|
22
|
+
*
|
|
23
|
+
* Empty-state copy is rendered when the list has no items, so the
|
|
24
|
+
* homepage never collapses to a blank section.
|
|
17
25
|
*/
|
|
18
|
-
|
|
19
|
-
import type { IndexProjectRecord } from "@grove-dev/core";
|
|
26
|
+
import type { IndexRecord } from "@grove-dev/core";
|
|
20
27
|
import ItemCard from "./ItemCard.astro";
|
|
28
|
+
import Container from "../layouts/Container.astro";
|
|
21
29
|
import SectionHeader from "../layouts/SectionHeader.astro";
|
|
22
30
|
|
|
23
31
|
interface Props {
|
|
@@ -25,9 +33,11 @@ interface Props {
|
|
|
25
33
|
id: string;
|
|
26
34
|
title: string;
|
|
27
35
|
description?: string;
|
|
28
|
-
items:
|
|
29
|
-
/** Detail
|
|
30
|
-
hrefFor
|
|
36
|
+
items: IndexRecord[];
|
|
37
|
+
/** Detail-page href builder (V1 published API). */
|
|
38
|
+
hrefFor?: (record: IndexRecord) => string;
|
|
39
|
+
/** Detail-page URL prefix (convenience API). */
|
|
40
|
+
pathPrefix?: string;
|
|
31
41
|
/** Optional "View all" link rendered in the header. */
|
|
32
42
|
viewAllHref?: string;
|
|
33
43
|
viewAllLabel?: string;
|
|
@@ -45,44 +55,70 @@ const {
|
|
|
45
55
|
description,
|
|
46
56
|
items,
|
|
47
57
|
hrefFor,
|
|
58
|
+
pathPrefix = "/items",
|
|
48
59
|
viewAllHref,
|
|
49
60
|
viewAllLabel = "View all",
|
|
50
61
|
eyebrow,
|
|
51
62
|
emptyLabel = "No records yet.",
|
|
52
63
|
class: className = "",
|
|
53
64
|
} = Astro.props;
|
|
65
|
+
|
|
66
|
+
const visible = items.slice(0, 6);
|
|
54
67
|
---
|
|
55
68
|
|
|
56
69
|
<section
|
|
57
70
|
id={id}
|
|
58
|
-
class
|
|
59
|
-
aria-labelledby={`${id}-heading`}
|
|
71
|
+
class={`grove-section scroll-mt-20 ${className}`}
|
|
60
72
|
>
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
<Container>
|
|
74
|
+
<div class="mb-6 flex flex-wrap items-end justify-between gap-3">
|
|
75
|
+
<SectionHeader
|
|
76
|
+
eyebrow={eyebrow}
|
|
77
|
+
title={title}
|
|
78
|
+
description={description}
|
|
79
|
+
/>
|
|
67
80
|
{viewAllHref && (
|
|
68
81
|
<a
|
|
69
|
-
slot="actions"
|
|
70
82
|
href={viewAllHref}
|
|
71
|
-
class="text-
|
|
83
|
+
class="hidden shrink-0 pb-2 text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
|
|
72
84
|
>
|
|
73
85
|
{viewAllLabel} →
|
|
74
86
|
</a>
|
|
75
87
|
)}
|
|
76
|
-
</
|
|
88
|
+
</div>
|
|
77
89
|
|
|
78
|
-
{
|
|
79
|
-
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
90
|
+
{visible.length === 0 ? (
|
|
91
|
+
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
92
|
+
{emptyLabel}
|
|
93
|
+
</p>
|
|
80
94
|
) : (
|
|
81
|
-
<div
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
95
|
+
<div
|
|
96
|
+
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
|
|
97
|
+
>
|
|
98
|
+
{visible.map((item) => {
|
|
99
|
+
// Resolve href: caller-supplied `hrefFor` callback wins;
|
|
100
|
+
// otherwise fall back to `${pathPrefix}/${slug}`. The href
|
|
101
|
+
// is what each card actually navigates to — getting it
|
|
102
|
+
// right is the difference between a working home page
|
|
103
|
+
// and a 404-storm of `/items/<slug>` links.
|
|
104
|
+
const base = pathPrefix.replace(/\/$/, "");
|
|
105
|
+
const fallbackHref = `${base}/${item.slug}`;
|
|
106
|
+
const finalHref =
|
|
107
|
+
typeof hrefFor === "function" ? hrefFor(item) : fallbackHref;
|
|
108
|
+
return (
|
|
109
|
+
<ItemCard record={item} href={finalHref} />
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
85
112
|
</div>
|
|
86
113
|
)}
|
|
87
|
-
|
|
114
|
+
|
|
115
|
+
{viewAllHref && (
|
|
116
|
+
<div class="mt-4 sm:hidden">
|
|
117
|
+
<a
|
|
118
|
+
href={viewAllHref}
|
|
119
|
+
class="text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
120
|
+
>{viewAllLabel} →</a>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</Container>
|
|
88
124
|
</section>
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
* repeated key. Empty values are dropped, so the URL stays clean.
|
|
12
12
|
*
|
|
13
13
|
* Generic: takes a `facets` map of `{ value, label, count }[]` per
|
|
14
|
-
* dimension and an `initial` `
|
|
14
|
+
* dimension and an `initial` `IndexFilters` for preselecting the
|
|
15
15
|
* current URL state.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type { IndexFilters } from "../lib/search";
|
|
19
19
|
import { SORT_OPTIONS } from "../lib/search";
|
|
20
|
+
import FilterGroupMenu from "./FilterGroupMenu.astro";
|
|
20
21
|
|
|
21
22
|
interface FacetValue {
|
|
22
23
|
value: string;
|
|
@@ -34,7 +35,7 @@ interface Facets {
|
|
|
34
35
|
|
|
35
36
|
interface Props {
|
|
36
37
|
facets: Facets;
|
|
37
|
-
initial:
|
|
38
|
+
initial: IndexFilters;
|
|
38
39
|
pathPrefix?: string;
|
|
39
40
|
class?: string;
|
|
40
41
|
/** Hide specific facets. Default: all shown. */
|
|
@@ -49,149 +50,122 @@ const {
|
|
|
49
50
|
hide = [],
|
|
50
51
|
} = Astro.props;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
interface FilterGroup {
|
|
54
|
+
label: string;
|
|
55
|
+
filterKey: "stacks" | "platforms" | "categories" | "licenses";
|
|
56
|
+
options: FacetValue[];
|
|
57
|
+
initial: string[];
|
|
58
|
+
single?: boolean;
|
|
59
|
+
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
---
|
|
61
|
+
interface RenderedFilterGroup extends FilterGroup {
|
|
62
|
+
active: boolean;
|
|
63
|
+
popoverId: string;
|
|
64
|
+
trigger: string;
|
|
65
|
+
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
]}
|
|
68
|
-
data-grove-refine-panel
|
|
69
|
-
>
|
|
70
|
-
{initial.q && <input type="hidden" name="q" value={initial.q} />}
|
|
71
|
-
{initial.lens && <input type="hidden" name="lens" value={initial.lens} />}
|
|
67
|
+
const baseGroups: FilterGroup[] = [
|
|
68
|
+
{ label: "Stack", filterKey: "stacks", options: facets.stacks ?? [], initial: initial.stacks ?? [] },
|
|
69
|
+
{ label: "Platform", filterKey: "platforms", options: facets.platforms ?? [], initial: initial.platforms ?? [] },
|
|
70
|
+
{ label: "Category", filterKey: "categories", options: facets.categories ?? [], initial: initial.categories ?? [] },
|
|
71
|
+
{ label: "License", filterKey: "licenses", options: facets.licenses ?? [], initial: initial.licenses ?? [], single: true },
|
|
72
|
+
];
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name="stack"
|
|
80
|
-
multiple
|
|
81
|
-
size="1"
|
|
82
|
-
onchange="this.form?.requestSubmit()"
|
|
83
|
-
class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm 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"
|
|
84
|
-
>
|
|
85
|
-
{facets.stacks!.map((f) => (
|
|
86
|
-
<option
|
|
87
|
-
value={f.value}
|
|
88
|
-
selected={initialStacks.has(f.value)}
|
|
89
|
-
>
|
|
90
|
-
{f.label ?? f.value}
|
|
91
|
-
{typeof f.count === "number" ? ` (${f.count})` : ""}
|
|
92
|
-
</option>
|
|
93
|
-
))}
|
|
94
|
-
</select>
|
|
95
|
-
</label>
|
|
96
|
-
)}
|
|
74
|
+
function pretty(value: string): string {
|
|
75
|
+
return value
|
|
76
|
+
.split(/[-_]/)
|
|
77
|
+
.map((part) => part ? part[0].toUpperCase() + part.slice(1) : part)
|
|
78
|
+
.join(" ");
|
|
79
|
+
}
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<select
|
|
104
|
-
name="platform"
|
|
105
|
-
multiple
|
|
106
|
-
size="1"
|
|
107
|
-
onchange="this.form?.requestSubmit()"
|
|
108
|
-
class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm 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"
|
|
109
|
-
>
|
|
110
|
-
{facets.platforms!.map((f) => (
|
|
111
|
-
<option
|
|
112
|
-
value={f.value}
|
|
113
|
-
selected={initialPlatforms.has(f.value)}
|
|
114
|
-
>
|
|
115
|
-
{f.label ?? f.value}
|
|
116
|
-
{typeof f.count === "number" ? ` (${f.count})` : ""}
|
|
117
|
-
</option>
|
|
118
|
-
))}
|
|
119
|
-
</select>
|
|
120
|
-
</label>
|
|
121
|
-
)}
|
|
81
|
+
function triggerLabel(group: FilterGroup): string {
|
|
82
|
+
if (group.initial.length === 0) return `${group.label}: Any`;
|
|
83
|
+
if (group.initial.length === 1) return `${group.label}: ${pretty(group.initial[0])}`;
|
|
84
|
+
return `${group.label}: ${group.initial.length} selected`;
|
|
85
|
+
}
|
|
122
86
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{facets.categories!.map((f) => (
|
|
136
|
-
<option
|
|
137
|
-
value={f.value}
|
|
138
|
-
selected={initialCategories.has(f.value)}
|
|
139
|
-
>
|
|
140
|
-
{f.label ?? f.value}
|
|
141
|
-
{typeof f.count === "number" ? ` (${f.count})` : ""}
|
|
142
|
-
</option>
|
|
143
|
-
))}
|
|
144
|
-
</select>
|
|
145
|
-
</label>
|
|
146
|
-
)}
|
|
87
|
+
const groups: RenderedFilterGroup[] = baseGroups
|
|
88
|
+
.filter((group) => {
|
|
89
|
+
const singular = group.filterKey.slice(0, -1) as "stack" | "platform" | "category" | "license";
|
|
90
|
+
return !hide.includes(singular) && group.options.length > 0;
|
|
91
|
+
})
|
|
92
|
+
.map((group) => ({
|
|
93
|
+
...group,
|
|
94
|
+
active: group.initial.length > 0,
|
|
95
|
+
popoverId: `filter-popover-${group.filterKey}`,
|
|
96
|
+
trigger: triggerLabel(group),
|
|
97
|
+
}));
|
|
98
|
+
---
|
|
147
99
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
License
|
|
152
|
-
</span>
|
|
153
|
-
<select
|
|
154
|
-
name="license"
|
|
155
|
-
multiple
|
|
156
|
-
size="1"
|
|
157
|
-
onchange="this.form?.requestSubmit()"
|
|
158
|
-
class="grove-select min-w-[10rem] rounded-md border border-ink-200 bg-white py-1.5 pl-2 pr-7 text-sm 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"
|
|
159
|
-
>
|
|
160
|
-
{facets.licenses!.map((f) => (
|
|
161
|
-
<option
|
|
162
|
-
value={f.value}
|
|
163
|
-
selected={initialLicenses.has(f.value)}
|
|
164
|
-
>
|
|
165
|
-
{f.label ?? f.value}
|
|
166
|
-
{typeof f.count === "number" ? ` (${f.count})` : ""}
|
|
167
|
-
</option>
|
|
168
|
-
))}
|
|
169
|
-
</select>
|
|
170
|
-
</label>
|
|
171
|
-
)}
|
|
100
|
+
<section class={className}>
|
|
101
|
+
<div class="flex flex-wrap items-center gap-2" role="toolbar" aria-label="Filter items">
|
|
102
|
+
{groups.map((group) => <FilterGroupMenu {...group} />)}
|
|
172
103
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
</option>
|
|
187
|
-
))}
|
|
188
|
-
</select>
|
|
189
|
-
</label>
|
|
190
|
-
)}
|
|
104
|
+
{!hide.includes("sort") && (
|
|
105
|
+
<label class="relative">
|
|
106
|
+
<span class="sr-only">Sort items</span>
|
|
107
|
+
<select name="sort" class="h-9 cursor-pointer appearance-none rounded-md border border-ink-200 bg-white py-1.5 pl-3 pr-8 text-[0.8125rem] font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300">
|
|
108
|
+
{SORT_OPTIONS.map((option) => <option value={option.value} selected={(initial.sort ?? "recently-updated") === option.value}>{option.label}</option>)}
|
|
109
|
+
</select>
|
|
110
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-400" fill="currentColor">
|
|
111
|
+
<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"/>
|
|
112
|
+
</svg>
|
|
113
|
+
</label>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
191
117
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
118
|
+
<script is:inline define:vars={{ pathPrefix }}>
|
|
119
|
+
(() => {
|
|
120
|
+
const groups = document.querySelectorAll(".grove-filter");
|
|
121
|
+
const keys = { stacks: "stack", platforms: "platform", categories: "category", licenses: "license" };
|
|
122
|
+
function closeAll(except) {
|
|
123
|
+
groups.forEach((group) => {
|
|
124
|
+
if (group === except) return;
|
|
125
|
+
const popover = group.querySelector(".grove-filter-popover");
|
|
126
|
+
const trigger = group.querySelector(".grove-filter-trigger");
|
|
127
|
+
if (popover && trigger) {
|
|
128
|
+
popover.hidden = true;
|
|
129
|
+
popover.classList.add("hidden");
|
|
130
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function navigate(group, values) {
|
|
135
|
+
const params = new URLSearchParams(location.search);
|
|
136
|
+
const key = keys[group.dataset.filterGroupKey];
|
|
137
|
+
params.delete(key);
|
|
138
|
+
params.delete("page");
|
|
139
|
+
values.forEach((value) => params.append(key, value));
|
|
140
|
+
location.assign(params.toString() ? `${pathPrefix}?${params}` : pathPrefix);
|
|
141
|
+
}
|
|
142
|
+
groups.forEach((group) => {
|
|
143
|
+
const trigger = group.querySelector(".grove-filter-trigger");
|
|
144
|
+
const popover = group.querySelector(".grove-filter-popover");
|
|
145
|
+
trigger?.addEventListener("click", (event) => {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
const opening = trigger.getAttribute("aria-expanded") !== "true";
|
|
148
|
+
closeAll(opening ? group : undefined);
|
|
149
|
+
popover.hidden = !opening;
|
|
150
|
+
popover.classList.toggle("hidden", !opening);
|
|
151
|
+
trigger.setAttribute("aria-expanded", String(opening));
|
|
152
|
+
});
|
|
153
|
+
group.querySelector(".grove-filter-apply")?.addEventListener("click", () => {
|
|
154
|
+
navigate(group, [...group.querySelectorAll(".grove-filter-input:checked")].map((input) => input.dataset.filterValue));
|
|
155
|
+
});
|
|
156
|
+
group.querySelector(".grove-filter-clear")?.addEventListener("click", () => navigate(group, []));
|
|
157
|
+
if (group.dataset.filterSingle === "true") {
|
|
158
|
+
group.querySelectorAll(".grove-filter-input").forEach((input) => input.addEventListener("change", () => navigate(group, [input.dataset.filterValue])));
|
|
159
|
+
}
|
|
160
|
+
popover?.addEventListener("click", (event) => event.stopPropagation());
|
|
161
|
+
});
|
|
162
|
+
document.addEventListener("click", () => closeAll());
|
|
163
|
+
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeAll(); });
|
|
164
|
+
document.querySelector('select[name="sort"]')?.addEventListener("change", (event) => {
|
|
165
|
+
const params = new URLSearchParams(location.search);
|
|
166
|
+
params.set("sort", event.target.value);
|
|
167
|
+
params.delete("page");
|
|
168
|
+
location.assign(params.toString() ? `${pathPrefix}?${params}` : pathPrefix);
|
|
169
|
+
});
|
|
170
|
+
})();
|
|
171
|
+
</script>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
2
3
|
/**
|
|
3
4
|
* StackGrid — browse-by-stack grid for the home page.
|
|
4
5
|
*
|
|
@@ -30,6 +31,7 @@ interface Props {
|
|
|
30
31
|
class?: string;
|
|
31
32
|
/** Hide the section eyebrow / header entirely. */
|
|
32
33
|
bare?: boolean;
|
|
34
|
+
itemLabelPlural?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const {
|
|
@@ -38,6 +40,7 @@ const {
|
|
|
38
40
|
iconAliases,
|
|
39
41
|
class: className = "",
|
|
40
42
|
bare = false,
|
|
43
|
+
itemLabelPlural = "items",
|
|
41
44
|
} = Astro.props;
|
|
42
45
|
|
|
43
46
|
function buildHref(slug: string): string {
|
|
@@ -48,7 +51,7 @@ function buildHref(slug: string): string {
|
|
|
48
51
|
function statusLabel(s: StackEntry["status"]): string | null {
|
|
49
52
|
switch (s) {
|
|
50
53
|
case "live":
|
|
51
|
-
return "
|
|
54
|
+
return "Available now";
|
|
52
55
|
case "expanding":
|
|
53
56
|
return "Expanding";
|
|
54
57
|
case "planned":
|
|
@@ -60,52 +63,57 @@ function statusLabel(s: StackEntry["status"]): string | null {
|
|
|
60
63
|
---
|
|
61
64
|
|
|
62
65
|
<section
|
|
63
|
-
|
|
66
|
+
id="stacks"
|
|
67
|
+
class:list={["grove-section scroll-mt-20 border-b border-ink-200 dark:border-ink-800", className]}
|
|
64
68
|
aria-labelledby={bare ? undefined : "grove-stackgrid-heading"}
|
|
65
69
|
>
|
|
66
|
-
<div class="grove-container
|
|
70
|
+
<div class="grove-container mx-auto px-5 sm:px-7">
|
|
67
71
|
{!bare && (
|
|
68
|
-
<header class="mb-6
|
|
72
|
+
<header class="mb-6">
|
|
73
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
74
|
+
Stacks
|
|
75
|
+
</span>
|
|
69
76
|
<h2
|
|
70
77
|
id="grove-stackgrid-heading"
|
|
71
|
-
class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
78
|
+
class="m-0 mt-1 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
72
79
|
>
|
|
73
80
|
Browse by stack
|
|
74
81
|
</h2>
|
|
75
82
|
<p class="m-0 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
76
|
-
|
|
77
|
-
stack to see what's shipping in that world.
|
|
83
|
+
Pick a stack to see real {itemLabelPlural} built with it.
|
|
78
84
|
</p>
|
|
79
85
|
</header>
|
|
80
86
|
)}
|
|
81
87
|
|
|
82
|
-
<div class="grid grid-cols-1 gap-
|
|
88
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
83
89
|
{stacks.map((s) => (
|
|
84
90
|
<a
|
|
85
91
|
href={buildHref(s.slug)}
|
|
86
|
-
class="
|
|
92
|
+
class="card card-hover group flex h-full items-center gap-3 p-3.5 no-underline"
|
|
87
93
|
data-event="stack_grid_click"
|
|
88
94
|
data-event-source="stack_grid"
|
|
89
95
|
>
|
|
90
|
-
<div class="flex items-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950" aria-hidden="true">
|
|
97
|
+
<Icon name={iconAliases?.[s.slug] ?? s.name} category="stack" size={18} />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
101
|
+
<div class="flex items-center gap-2">
|
|
102
|
+
<h3 class="m-0 text-[0.875rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
103
|
+
{s.name}
|
|
104
|
+
</h3>
|
|
105
|
+
{typeof s.count === "number" && (
|
|
106
|
+
<span class="inline-flex items-center rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ink-600 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-400">
|
|
107
|
+
{s.count} {itemLabelPlural}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
{s.blurb && <p class="m-0 truncate text-2xs text-ink-500 dark:text-ink-400">{s.blurb}</p>}
|
|
99
112
|
</div>
|
|
100
|
-
{s.blurb && (
|
|
101
|
-
<p class="m-0 line-clamp-2 text-[0.8125rem] leading-snug text-ink-500 dark:text-ink-400">
|
|
102
|
-
{s.blurb}
|
|
103
|
-
</p>
|
|
104
|
-
)}
|
|
105
113
|
{statusLabel(s.status) && (
|
|
106
114
|
<span
|
|
107
115
|
class:list={[
|
|
108
|
-
"
|
|
116
|
+
"shrink-0 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider",
|
|
109
117
|
s.status === "live"
|
|
110
118
|
? "grove-badge-new"
|
|
111
119
|
: s.status === "expanding"
|