@grove-dev/astro 0.2.14 → 0.2.16
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/package.json +3 -3
- package/src/components/ItemCard.astro +33 -5
- package/templates/default/package.json +3 -3
- package/templates/default/public/llms-full.txt +1 -1
- package/templates/default/public/sitemap.xml +10 -10
- package/templates/default/src/pages/[slug]/[recordSlug].astro +47 -0
- package/templates/default/src/pages/[slug]/index.astro +95 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grove-dev/astro",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Astro framework adapter for Grove. Components, layouts, design tokens, and a default template.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"./layouts/*": "./src/layouts/*.astro"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@grove-dev/core": "0.2.
|
|
47
|
-
"@grove-dev/ui": "1.0.
|
|
46
|
+
"@grove-dev/core": "0.2.16",
|
|
47
|
+
"@grove-dev/ui": "1.0.6",
|
|
48
48
|
"astro": "^6.4.6"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
* /resources/<slug>, ecosystem-map → /entities/<slug>) pass the
|
|
11
11
|
* correct href via `RecordSection`'s `hrefFor` callback.
|
|
12
12
|
*
|
|
13
|
+
* V1 (alternative) API: pass `routeSlug` (e.g. `"projects"`,
|
|
14
|
+
* `"resources"`, `"entities"`) to let the component derive the
|
|
15
|
+
* detail URL itself as `/${routeSlug}/${slug}`. This is a thin
|
|
16
|
+
* alternative to passing `href`; the page-level `hrefFor` callback
|
|
17
|
+
* is still the recommended API.
|
|
18
|
+
*
|
|
13
19
|
* V1 (legacy) API: also accepts `item` + `pathPrefix` as a
|
|
14
|
-
* convenience for callers that haven't migrated. When
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* convenience for callers that haven't migrated. When any of
|
|
21
|
+
* `href` / `routeSlug` / `pathPrefix` is provided, the priority is
|
|
22
|
+
* `href` > `routeSlug` > `pathPrefix`. The `pathPrefix` default
|
|
23
|
+
* is `"/items"` for V0 backwards-compat, but the canonical V1
|
|
24
|
+
* pattern is to pass `routeSlug` (or `href` directly).
|
|
17
25
|
*/
|
|
18
26
|
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
19
27
|
import Icon from "./Icon.astro";
|
|
@@ -26,12 +34,16 @@ type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
|
|
|
26
34
|
interface Props {
|
|
27
35
|
/** V1 API — full Resource/IndexProjectRecord. */
|
|
28
36
|
record?: CardRecord;
|
|
29
|
-
/** V1 API — full detail-page URL. */
|
|
37
|
+
/** V1 API — full detail-page URL. Wins over `routeSlug` / `pathPrefix`. */
|
|
30
38
|
href?: string;
|
|
39
|
+
/** V1 API — blueprint route slug (e.g. "projects", "resources",
|
|
40
|
+
* "entities"). When `href` is not provided, the detail URL is
|
|
41
|
+
* derived as `/${routeSlug}/${slug}`. */
|
|
42
|
+
routeSlug?: string;
|
|
31
43
|
/** Legacy API — singular item, equivalent to `record`. */
|
|
32
44
|
item?: CardRecord;
|
|
33
45
|
/** Legacy API — URL prefix, equivalent to deriving from
|
|
34
|
-
* blueprint routeSlug + slug. */
|
|
46
|
+
* blueprint routeSlug + slug. Default `"/items"` (V0 leftover). */
|
|
35
47
|
pathPrefix?: string;
|
|
36
48
|
/** When true, show owner/repo below the title. Default true. */
|
|
37
49
|
showOwner?: boolean;
|
|
@@ -47,6 +59,7 @@ interface Props {
|
|
|
47
59
|
const {
|
|
48
60
|
record: recordProp,
|
|
49
61
|
href: hrefProp,
|
|
62
|
+
routeSlug,
|
|
50
63
|
item: itemProp,
|
|
51
64
|
pathPrefix = "/items",
|
|
52
65
|
showOwner = true,
|
|
@@ -62,7 +75,12 @@ const slug = (item as { slug: string }).slug;
|
|
|
62
75
|
let detailHref: string;
|
|
63
76
|
if (typeof hrefProp === "string" && hrefProp.length > 0) {
|
|
64
77
|
detailHref = hrefProp;
|
|
78
|
+
} else if (typeof routeSlug === "string" && routeSlug.length > 0) {
|
|
79
|
+
// V1 alternative API: blueprint routeSlug → /<routeSlug>/<slug>.
|
|
80
|
+
const base = routeSlug.replace(/^\/+|\/+$/g, "");
|
|
81
|
+
detailHref = base ? `/${base}/${slug}` : `/${slug}`;
|
|
65
82
|
} else {
|
|
83
|
+
// V0 legacy fallback: explicit `pathPrefix` (default "/items").
|
|
66
84
|
const base = pathPrefix.replace(/\/$/, "");
|
|
67
85
|
detailHref = `${base}/${slug}`;
|
|
68
86
|
}
|
|
@@ -187,6 +205,16 @@ const category = (item as { category?: string }).category;
|
|
|
187
205
|
data-event-item={slug}
|
|
188
206
|
class="relative z-10 font-mono text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
189
207
|
>
|
|
208
|
+
{/* `data-event` and `data-event-*` are forwarding hooks
|
|
209
|
+
for downstream analytics. Sites wire their own
|
|
210
|
+
gtag/PostHog/Plausible handler in BaseLayout.astro
|
|
211
|
+
to read these and forward to their analytics
|
|
212
|
+
provider. The V1 names are stable: `data-event` is
|
|
213
|
+
the action ("external_repo_click"), `data-event-source`
|
|
214
|
+
is the surface ("card_owner" | "card_view_repo" |
|
|
215
|
+
"detail_owner" | "detail_view_github" | "detail_homepage"
|
|
216
|
+
| "detail_distribution"), and `data-event-item` is
|
|
217
|
+
the record slug. */}
|
|
190
218
|
<span class="text-ink-400 dark:text-ink-500">@</span>{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
|
|
191
219
|
</a>
|
|
192
220
|
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@astrojs/check": "^0.9.9",
|
|
28
|
-
"@grove-dev/astro": "0.2.
|
|
29
|
-
"@grove-dev/cli": "0.2.
|
|
30
|
-
"@grove-dev/core": "0.2.
|
|
28
|
+
"@grove-dev/astro": "0.2.16",
|
|
29
|
+
"@grove-dev/cli": "0.2.16",
|
|
30
|
+
"@grove-dev/core": "0.2.16",
|
|
31
31
|
"@tailwindcss/vite": "^4.3.0",
|
|
32
32
|
"astro": "^6.4.4",
|
|
33
33
|
"marked": "^18.0.0",
|
|
@@ -2,61 +2,61 @@
|
|
|
2
2
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
3
|
<url>
|
|
4
4
|
<loc>https://example.com/</loc>
|
|
5
|
-
<lastmod>2026-06-
|
|
5
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
6
6
|
<changefreq>daily</changefreq>
|
|
7
7
|
<priority>1.0</priority>
|
|
8
8
|
</url>
|
|
9
9
|
<url>
|
|
10
10
|
<loc>https://example.com/projects</loc>
|
|
11
|
-
<lastmod>2026-06-
|
|
11
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
12
12
|
<changefreq>daily</changefreq>
|
|
13
13
|
<priority>0.9</priority>
|
|
14
14
|
</url>
|
|
15
15
|
<url>
|
|
16
16
|
<loc>https://example.com/projects/coolify</loc>
|
|
17
|
-
<lastmod>2026-06-
|
|
17
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
18
18
|
<changefreq>weekly</changefreq>
|
|
19
19
|
<priority>0.7</priority>
|
|
20
20
|
</url>
|
|
21
21
|
<url>
|
|
22
22
|
<loc>https://example.com/projects/gitea</loc>
|
|
23
|
-
<lastmod>2026-06-
|
|
23
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
24
24
|
<changefreq>weekly</changefreq>
|
|
25
25
|
<priority>0.7</priority>
|
|
26
26
|
</url>
|
|
27
27
|
<url>
|
|
28
28
|
<loc>https://example.com/projects/hoppscotch</loc>
|
|
29
|
-
<lastmod>2026-06-
|
|
29
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
30
30
|
<changefreq>weekly</changefreq>
|
|
31
31
|
<priority>0.7</priority>
|
|
32
32
|
</url>
|
|
33
33
|
<url>
|
|
34
34
|
<loc>https://example.com/projects/inventree</loc>
|
|
35
|
-
<lastmod>2026-06-
|
|
35
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
36
36
|
<changefreq>weekly</changefreq>
|
|
37
37
|
<priority>0.7</priority>
|
|
38
38
|
</url>
|
|
39
39
|
<url>
|
|
40
40
|
<loc>https://example.com/projects/label-studio</loc>
|
|
41
|
-
<lastmod>2026-06-
|
|
41
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
42
42
|
<changefreq>weekly</changefreq>
|
|
43
43
|
<priority>0.7</priority>
|
|
44
44
|
</url>
|
|
45
45
|
<url>
|
|
46
46
|
<loc>https://example.com/projects/logseq</loc>
|
|
47
|
-
<lastmod>2026-06-
|
|
47
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
48
48
|
<changefreq>weekly</changefreq>
|
|
49
49
|
<priority>0.7</priority>
|
|
50
50
|
</url>
|
|
51
51
|
<url>
|
|
52
52
|
<loc>https://example.com/projects/private-gpt</loc>
|
|
53
|
-
<lastmod>2026-06-
|
|
53
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
54
54
|
<changefreq>weekly</changefreq>
|
|
55
55
|
<priority>0.7</priority>
|
|
56
56
|
</url>
|
|
57
57
|
<url>
|
|
58
58
|
<loc>https://example.com/projects/wakapi</loc>
|
|
59
|
-
<lastmod>2026-06-
|
|
59
|
+
<lastmod>2026-06-15T13:49:38.754Z</lastmod>
|
|
60
60
|
<changefreq>weekly</changefreq>
|
|
61
61
|
<priority>0.7</priority>
|
|
62
62
|
</url>
|
|
@@ -162,20 +162,51 @@ const languages = extras.github?.languages
|
|
|
162
162
|
const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, b]) => sum + b, 0));
|
|
163
163
|
|
|
164
164
|
// ── Kind-aware JSON-LD ─────────────────────────────────────────
|
|
165
|
+
// Mirrors the openapps field set (10+ fields for projects):
|
|
166
|
+
// name, headline, description, url, codeRepository, sameAs,
|
|
167
|
+
// programmingLanguage, license, applicationCategory,
|
|
168
|
+
// operatingSystem, keywords, dateCreated, dateModified,
|
|
169
|
+
// interactionStatistic, author, isAccessibleForFree.
|
|
165
170
|
let jsonLd: Record<string, unknown>;
|
|
166
171
|
if (isProject && proj) {
|
|
172
|
+
const sameAs = [repoUrl, homepageUrl].filter((value): value is string =>
|
|
173
|
+
typeof value === "string" && value.length > 0,
|
|
174
|
+
);
|
|
175
|
+
const keywords = (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined;
|
|
176
|
+
const operatingSystem = (proj.platforms ?? []).length > 0
|
|
177
|
+
? (proj.platforms ?? []).join(", ")
|
|
178
|
+
: undefined;
|
|
179
|
+
const applicationCategory = (record.category ?? "") || undefined;
|
|
180
|
+
const dateCreated = (record as { curation?: { reviewedAt?: string } }).curation?.reviewedAt;
|
|
181
|
+
const dateModified = pushedAt ?? dateCreated;
|
|
182
|
+
const interactionStatistic = stars > 0
|
|
183
|
+
? {
|
|
184
|
+
"@type": "InteractionCounter",
|
|
185
|
+
interactionType: { "@type": "LikeAction" },
|
|
186
|
+
userInteractionCount: stars,
|
|
187
|
+
}
|
|
188
|
+
: undefined;
|
|
167
189
|
jsonLd = {
|
|
168
190
|
"@context": "https://schema.org",
|
|
169
191
|
"@type": "SoftwareSourceCode",
|
|
170
192
|
name,
|
|
193
|
+
headline: name,
|
|
171
194
|
description: record.description ?? undefined,
|
|
172
195
|
url: repoUrl || homepageUrl || undefined,
|
|
173
196
|
codeRepository: repoUrl || undefined,
|
|
197
|
+
sameAs: sameAs.length > 0 ? sameAs : undefined,
|
|
174
198
|
programmingLanguage: language ?? undefined,
|
|
175
199
|
license: licenseSpdx ?? undefined,
|
|
200
|
+
applicationCategory,
|
|
201
|
+
operatingSystem,
|
|
202
|
+
keywords,
|
|
203
|
+
dateCreated: dateCreated ?? undefined,
|
|
204
|
+
dateModified: dateModified ?? undefined,
|
|
205
|
+
interactionStatistic,
|
|
176
206
|
author: ownerRepo
|
|
177
207
|
? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
|
|
178
208
|
: undefined,
|
|
209
|
+
isAccessibleForFree: true,
|
|
179
210
|
};
|
|
180
211
|
} else if (record.kind === "resource") {
|
|
181
212
|
const resourceSchemaType: Record<string, string> = {
|
|
@@ -189,20 +220,25 @@ if (isProject && proj) {
|
|
|
189
220
|
"@context": "https://schema.org",
|
|
190
221
|
"@type": resourceSchemaType[record.type] ?? "CreativeWork",
|
|
191
222
|
name,
|
|
223
|
+
headline: name,
|
|
192
224
|
description: record.description || undefined,
|
|
193
225
|
url: homepageUrl || undefined,
|
|
194
226
|
author: record.author ? { "@type": "Person", name: record.author } : undefined,
|
|
195
227
|
datePublished: record.publishedAt || undefined,
|
|
228
|
+
keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
|
|
229
|
+
isAccessibleForFree: true,
|
|
196
230
|
};
|
|
197
231
|
} else {
|
|
198
232
|
jsonLd = {
|
|
199
233
|
"@context": "https://schema.org",
|
|
200
234
|
"@type": record.type === "person" ? "Person" : "Organization",
|
|
201
235
|
name,
|
|
236
|
+
headline: name,
|
|
202
237
|
description: record.description || undefined,
|
|
203
238
|
url: homepageUrl || undefined,
|
|
204
239
|
foundingDate: record.founded || undefined,
|
|
205
240
|
location: record.location || undefined,
|
|
241
|
+
keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
|
|
206
242
|
};
|
|
207
243
|
}
|
|
208
244
|
---
|
|
@@ -264,6 +300,17 @@ if (isProject && proj) {
|
|
|
264
300
|
{labelDisplay(l) ?? prettySlug(l)}
|
|
265
301
|
</span>
|
|
266
302
|
))}
|
|
303
|
+
{record.curation?.reviewed && (
|
|
304
|
+
<span
|
|
305
|
+
class="inline-flex items-center gap-1 rounded border border-emerald-300/60 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300"
|
|
306
|
+
title="A human curator reviewed this record. (No reviewer name is shown — the framework is reviewer-agnostic; consumers can wire the curation.by field into their own badge component.)"
|
|
307
|
+
>
|
|
308
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
309
|
+
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z" />
|
|
310
|
+
</svg>
|
|
311
|
+
Curator reviewed
|
|
312
|
+
</span>
|
|
313
|
+
)}
|
|
267
314
|
{healthLabel && (
|
|
268
315
|
<span class="grove-badge grove-badge-stale">{healthLabel}</span>
|
|
269
316
|
)}
|
|
@@ -21,6 +21,7 @@ import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
|
21
21
|
import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
|
|
22
22
|
import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
|
|
23
23
|
import IndexRow from "@grove-dev/astro/components/IndexRow.astro";
|
|
24
|
+
import Pagination from "@grove-dev/astro/components/Pagination.astro";
|
|
24
25
|
import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
|
|
25
26
|
import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
|
|
26
27
|
import {
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
paginate,
|
|
37
38
|
searchParamsFromFilters,
|
|
38
39
|
sortDisplay,
|
|
40
|
+
SORT_OPTIONS,
|
|
39
41
|
totalPages,
|
|
40
42
|
} from "@grove-dev/astro";
|
|
41
43
|
|
|
@@ -165,6 +167,7 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
165
167
|
<form
|
|
166
168
|
method="get"
|
|
167
169
|
action={`/${slug}`}
|
|
170
|
+
id="search-form"
|
|
168
171
|
class="mt-6 flex flex-col gap-2 sm:flex-row"
|
|
169
172
|
role="search"
|
|
170
173
|
>
|
|
@@ -178,9 +181,10 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
178
181
|
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
|
|
179
182
|
fill="currentColor"
|
|
180
183
|
>
|
|
181
|
-
<path 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.
|
|
184
|
+
<path 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.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z" />
|
|
182
185
|
</svg>
|
|
183
186
|
<input
|
|
187
|
+
id="search-input"
|
|
184
188
|
type="search"
|
|
185
189
|
name="q"
|
|
186
190
|
value={filters.q ?? ""}
|
|
@@ -188,7 +192,28 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
188
192
|
class="h-10 w-full rounded-md border border-ink-200 bg-white py-2 pl-9 pr-3 text-sm 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"
|
|
189
193
|
/>
|
|
190
194
|
</label>
|
|
191
|
-
|
|
195
|
+
<label class="relative">
|
|
196
|
+
<span class="sr-only">{`Sort ${itemLabelPlural()}`}</span>
|
|
197
|
+
<select
|
|
198
|
+
id="sort-select"
|
|
199
|
+
name="sort"
|
|
200
|
+
class="h-10 cursor-pointer appearance-none rounded-md border border-ink-200 bg-white py-1.5 pl-3 pr-8 text-sm font-medium text-ink-700 transition-colors hover:border-ink-300 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-300 dark:hover:border-ink-700 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
|
|
201
|
+
>
|
|
202
|
+
{SORT_OPTIONS.map((o) => (
|
|
203
|
+
<option value={o.value} selected={o.value === sort}>{o.label}</option>
|
|
204
|
+
))}
|
|
205
|
+
</select>
|
|
206
|
+
<svg
|
|
207
|
+
viewBox="0 0 16 16"
|
|
208
|
+
width="11"
|
|
209
|
+
height="11"
|
|
210
|
+
aria-hidden="true"
|
|
211
|
+
class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
212
|
+
fill="currentColor"
|
|
213
|
+
>
|
|
214
|
+
<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" />
|
|
215
|
+
</svg>
|
|
216
|
+
</label>
|
|
192
217
|
<button type="submit" class="grove-btn grove-btn-primary">Search</button>
|
|
193
218
|
</form>
|
|
194
219
|
|
|
@@ -278,7 +303,28 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
278
303
|
</p>
|
|
279
304
|
<a href={`/${slug}`} class="grove-btn grove-btn-outline mt-4">Clear filters</a>
|
|
280
305
|
</div>
|
|
281
|
-
<nav
|
|
306
|
+
<nav class="mt-8 flex flex-wrap items-center justify-center gap-1.5" aria-label="Pagination">
|
|
307
|
+
<Pagination current={page} total={pages} hrefFor={hrefForPage} />
|
|
308
|
+
</nav>
|
|
309
|
+
</div>
|
|
310
|
+
</section>
|
|
311
|
+
|
|
312
|
+
<section class="border-t border-ink-200 dark:border-ink-800">
|
|
313
|
+
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
314
|
+
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
|
315
|
+
<h2 class="m-0 text-xl font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
316
|
+
{`Know a real ${itemLabel()} that belongs here?`}
|
|
317
|
+
</h2>
|
|
318
|
+
<p class="m-0 max-w-2xl text-sm text-ink-500 dark:text-ink-400">
|
|
319
|
+
{`Open a pull request with a YAML file. We review every submission against the bar — no marketing, no tutorials.`}
|
|
320
|
+
</p>
|
|
321
|
+
<a
|
|
322
|
+
href={siteConfig.submitUrl ?? "/submit"}
|
|
323
|
+
class="grove-btn grove-btn-primary mt-2"
|
|
324
|
+
>
|
|
325
|
+
{`Submit ${itemLabel()}`}
|
|
326
|
+
</a>
|
|
327
|
+
</div>
|
|
282
328
|
</div>
|
|
283
329
|
</section>
|
|
284
330
|
|
|
@@ -288,6 +334,49 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
288
334
|
var list = document.getElementById("results-list");
|
|
289
335
|
if (!list) return;
|
|
290
336
|
|
|
337
|
+
// Sort dropdown: navigate on change so the server re-renders
|
|
338
|
+
// the sorted list (URL is the source of truth for filters/sort).
|
|
339
|
+
var sortSelect = document.getElementById("sort-select");
|
|
340
|
+
if (sortSelect) {
|
|
341
|
+
sortSelect.addEventListener("change", function () {
|
|
342
|
+
var params = new URLSearchParams(location.search);
|
|
343
|
+
if (sortSelect.value && sortSelect.value !== "recently-updated") {
|
|
344
|
+
params.set("sort", sortSelect.value);
|
|
345
|
+
} else {
|
|
346
|
+
params.delete("sort");
|
|
347
|
+
}
|
|
348
|
+
params.delete("page");
|
|
349
|
+
var qs = params.toString();
|
|
350
|
+
location.assign(qs ? "?" + qs : location.pathname);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Search: 250ms debounce; pressing Enter still submits
|
|
355
|
+
// immediately via the form's normal submit handler.
|
|
356
|
+
var searchInput = document.getElementById("search-input");
|
|
357
|
+
var searchForm = document.getElementById("search-form");
|
|
358
|
+
if (searchInput && searchForm) {
|
|
359
|
+
var debounceTimer = null;
|
|
360
|
+
var submit = function () {
|
|
361
|
+
var params = new URLSearchParams(location.search);
|
|
362
|
+
var term = searchInput.value.trim();
|
|
363
|
+
if (term) params.set("q", term);
|
|
364
|
+
else params.delete("q");
|
|
365
|
+
params.delete("page");
|
|
366
|
+
var qs = params.toString();
|
|
367
|
+
location.assign(qs ? "?" + qs : location.pathname);
|
|
368
|
+
};
|
|
369
|
+
searchInput.addEventListener("input", function () {
|
|
370
|
+
if (debounceTimer) window.clearTimeout(debounceTimer);
|
|
371
|
+
debounceTimer = window.setTimeout(submit, 250);
|
|
372
|
+
});
|
|
373
|
+
searchForm.addEventListener("submit", function (event) {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
if (debounceTimer) window.clearTimeout(debounceTimer);
|
|
376
|
+
submit();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
291
380
|
function values(params, key) {
|
|
292
381
|
return params.getAll(key).filter(Boolean);
|
|
293
382
|
}
|
|
@@ -396,21 +485,9 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
396
485
|
});
|
|
397
486
|
});
|
|
398
487
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (totalPages > 1) {
|
|
403
|
-
for (var number = 1; number <= totalPages; number += 1) {
|
|
404
|
-
var link = document.createElement("a");
|
|
405
|
-
link.href = pageHref(params, number);
|
|
406
|
-
link.textContent = String(number);
|
|
407
|
-
link.className = "grove-btn min-w-9 px-2 py-1.5 text-2xs " +
|
|
408
|
-
(number === page ? "grove-btn-primary" : "grove-btn-outline");
|
|
409
|
-
if (number === page) link.setAttribute("aria-current", "page");
|
|
410
|
-
pager.appendChild(link);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
488
|
+
// Pagination is server-rendered (see <Pagination /> in the
|
|
489
|
+
// template) so the static HTML contains the page-number
|
|
490
|
+
// links for SEO. No client-side rendering loop here.
|
|
414
491
|
|
|
415
492
|
var chipHost = document.getElementById("active-filters");
|
|
416
493
|
if (chipHost) {
|