@grove-dev/astro 0.5.0-next.2 → 0.5.0
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/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +0 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/ui/button.d.ts +45 -0
- package/dist/ui/button.d.ts.map +1 -0
- package/dist/ui/button.js +82 -0
- package/dist/ui/button.js.map +1 -0
- package/package.json +8 -5
- package/src/components/CardGrid.astro +41 -0
- package/src/components/CardIcon.astro +67 -0
- package/src/components/CategoryGrid.astro +20 -5
- package/src/components/CollectionCard.astro +56 -0
- package/src/components/CollectionIndex.astro +21 -26
- package/src/components/CollectionPage.astro +36 -13
- package/src/components/CollectionRow.astro +62 -192
- package/src/components/CollectionTeaser.astro +4 -0
- package/src/components/ContributorsGrid.astro +4 -1
- package/src/components/DirectoryIndexClient.astro +127 -30
- package/src/components/EditorialSummary.astro +6 -5
- package/src/components/FilterGroupMenu.astro +24 -10
- package/src/components/FilterOptions.astro +6 -1
- package/src/components/FinalCta.astro +5 -3
- package/src/components/Hero.astro +85 -139
- package/src/components/IndexRow.astro +36 -69
- package/src/components/LanguageBreakdown.astro +5 -1
- package/src/components/OriginalCollection.astro +3 -2
- package/src/components/Pagination.astro +7 -20
- package/src/components/ProjectCard.astro +345 -0
- package/src/components/RecordHeader.astro +111 -82
- package/src/components/RecordSection.astro +14 -10
- package/src/components/RecordSidebar.astro +68 -20
- package/src/components/RefinePanel.astro +41 -62
- package/src/components/SmartLensTabs.astro +3 -7
- package/src/components/StackGrid.astro +26 -6
- package/src/components/SubmissionClient.astro +140 -63
- package/src/components/TableOfContents.astro +31 -5
- package/src/components/WhyThisExists.astro +1 -1
- package/src/index.ts +2 -2
- package/src/layouts/BaseLayout.astro +79 -9
- package/src/layouts/Header.astro +5 -4
- package/src/layouts/SectionHeader.astro +3 -2
- package/src/layouts/Seo.astro +28 -17
- package/src/layouts/ThemeToggle.astro +23 -6
- package/src/lib/index.ts +0 -2
- package/src/server/collections.ts +11 -0
- package/src/server/contrast.test.ts +82 -0
- package/src/server/contrast.ts +117 -0
- package/src/server/directory.test.ts +177 -0
- package/src/server/directory.ts +328 -206
- package/src/server/github-repo.test.ts +88 -0
- package/src/server/github-repo.ts +104 -0
- package/src/server/index.ts +4 -3
- package/src/server/models-home.test.ts +101 -0
- package/src/server/models.test.ts +135 -0
- package/src/server/models.ts +170 -42
- package/src/styles.css +128 -16
- package/dist/lib/scores.d.ts +0 -2
- package/dist/lib/scores.d.ts.map +0 -1
- package/dist/lib/scores.js +0 -2
- package/dist/lib/scores.js.map +0 -1
- package/src/components/CurationGrid.astro +0 -41
- package/src/components/DecisionRow.astro +0 -89
- package/src/components/ExploreByCategory.astro +0 -67
- package/src/components/ExploreByStack.astro +0 -78
- package/src/components/GroveDocumentHead.astro +0 -28
- package/src/components/ItemCard.astro +0 -324
- package/src/components/MinimalAbout.astro +0 -82
- package/src/components/ScoreBars.astro +0 -102
- package/src/lib/scores.ts +0 -1
|
@@ -8,7 +8,10 @@ interface Props {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const { clientItemsJson, slug, singular, plural, taxonomy } = Astro.props;
|
|
11
|
-
const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).replace(
|
|
11
|
+
const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).replace(
|
|
12
|
+
/</g,
|
|
13
|
+
'\\u003c',
|
|
14
|
+
);
|
|
12
15
|
---
|
|
13
16
|
|
|
14
17
|
<script is:inline id="grove-index-data" type="application/json" set:html={clientItemsJson}></script>
|
|
@@ -19,17 +22,26 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
19
22
|
PAGE_SIZE,
|
|
20
23
|
activeFilterChips,
|
|
21
24
|
applySort,
|
|
25
|
+
buildFacets,
|
|
22
26
|
effectivePage,
|
|
23
27
|
effectiveSort,
|
|
24
28
|
filterRecords,
|
|
25
29
|
filtersFromSearchParams,
|
|
26
30
|
hrefForLens,
|
|
27
31
|
isLensActive,
|
|
32
|
+
paginationPageList,
|
|
28
33
|
removeFilter,
|
|
29
34
|
searchParamsFromFilters,
|
|
30
35
|
sortDisplay,
|
|
31
36
|
totalPages,
|
|
32
37
|
} from "@grove-dev/core/directory";
|
|
38
|
+
import {
|
|
39
|
+
PAGINATION_EXTRA,
|
|
40
|
+
buttonClass,
|
|
41
|
+
chipClass,
|
|
42
|
+
filterTriggerClass,
|
|
43
|
+
lensTabClass,
|
|
44
|
+
} from "../ui/button.js";
|
|
33
45
|
|
|
34
46
|
(() => {
|
|
35
47
|
const source = document.querySelector("#grove-index-data");
|
|
@@ -39,8 +51,12 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
39
51
|
|
|
40
52
|
const items = JSON.parse(source.textContent || "[]");
|
|
41
53
|
const { slug, singular, plural, taxonomy } = JSON.parse(configSource.textContent || "{}");
|
|
54
|
+
// `:scope >` keeps the lookup on the page's own `<li>` wrappers —
|
|
55
|
+
// the card component inside each item also carries
|
|
56
|
+
// `data-record-slug`, and appending/hiding the inner card instead
|
|
57
|
+
// of its wrapper would tear the list apart.
|
|
42
58
|
const nodes = new Map(
|
|
43
|
-
[...list.querySelectorAll("[data-record-slug]")].map((node) => [
|
|
59
|
+
[...list.querySelectorAll(":scope > [data-record-slug]")].map((node) => [
|
|
44
60
|
(node as HTMLElement).dataset.recordSlug,
|
|
45
61
|
node,
|
|
46
62
|
]),
|
|
@@ -64,9 +80,22 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
64
80
|
.map((record) => record.slug),
|
|
65
81
|
);
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
// Re-append only when the visible order has changed. On every
|
|
84
|
+
// keystroke / filter change this would otherwise thrash the
|
|
85
|
+
// DOM order even when the sort + filter result is identical
|
|
86
|
+
// to the previous run.
|
|
87
|
+
const currentOrder = [...list.querySelectorAll(":scope > [data-record-slug]")].map(
|
|
88
|
+
(node) => (node as HTMLElement).dataset.recordSlug,
|
|
89
|
+
);
|
|
90
|
+
const nextOrder = filtered.map((record) => record.slug);
|
|
91
|
+
const orderChanged =
|
|
92
|
+
currentOrder.length !== nextOrder.length ||
|
|
93
|
+
currentOrder.some((slug, index) => slug !== nextOrder[index]);
|
|
94
|
+
if (orderChanged) {
|
|
95
|
+
for (const record of filtered) {
|
|
96
|
+
const node = nodes.get(record.slug) as HTMLElement | undefined;
|
|
97
|
+
if (node) list.append(node);
|
|
98
|
+
}
|
|
70
99
|
}
|
|
71
100
|
for (const [recordSlug, node] of nodes) {
|
|
72
101
|
(node as HTMLElement).hidden = !visible.has(recordSlug);
|
|
@@ -81,20 +110,56 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
81
110
|
if (pageLabel) pageLabel.textContent = pageCount > 1 ? ` · page ${page} of ${pageCount}` : "";
|
|
82
111
|
if (empty) empty.classList.toggle("hidden", filtered.length > 0);
|
|
83
112
|
|
|
113
|
+
// Rebuild the pagination controls, mirroring the server-rendered
|
|
114
|
+
// `Pagination.astro` (Previous/Next + windowed page list via the
|
|
115
|
+
// shared `paginationPageList`) so client filtering never degrades
|
|
116
|
+
// the affordances of the initial render.
|
|
84
117
|
const pagination = document.querySelector("#results-pagination");
|
|
85
118
|
if (pagination) {
|
|
86
119
|
pagination.innerHTML = "";
|
|
87
120
|
if (pageCount > 1) {
|
|
88
|
-
|
|
121
|
+
const linkClass = buttonClass("secondary", "sm", PAGINATION_EXTRA);
|
|
122
|
+
const hrefFor = (target: number) => {
|
|
89
123
|
const next = searchParamsFromFilters({ ...filters, page: target });
|
|
124
|
+
return `/${slug}${next.size ? `?${next}` : ""}`;
|
|
125
|
+
};
|
|
126
|
+
const nav = document.createElement("nav");
|
|
127
|
+
nav.className = "flex flex-wrap items-center justify-center gap-1.5";
|
|
128
|
+
nav.setAttribute("aria-label", "Pagination");
|
|
129
|
+
const control = (label: string, target: number | null, rel?: string) => {
|
|
130
|
+
if (target === null) {
|
|
131
|
+
const span = document.createElement("span");
|
|
132
|
+
span.className = `${linkClass} pointer-events-none opacity-40`;
|
|
133
|
+
span.textContent = label;
|
|
134
|
+
return span;
|
|
135
|
+
}
|
|
90
136
|
const link = document.createElement("a");
|
|
91
|
-
link.href =
|
|
92
|
-
link.textContent =
|
|
93
|
-
link.
|
|
94
|
-
if (
|
|
95
|
-
link
|
|
96
|
-
|
|
137
|
+
link.href = hrefFor(target);
|
|
138
|
+
link.textContent = label;
|
|
139
|
+
link.className = linkClass;
|
|
140
|
+
if (rel) link.rel = rel;
|
|
141
|
+
return link;
|
|
142
|
+
};
|
|
143
|
+
nav.append(control("Previous", page > 1 ? page - 1 : null, "prev"));
|
|
144
|
+
for (const entry of paginationPageList(page, pageCount)) {
|
|
145
|
+
if (entry === "ellipsis") {
|
|
146
|
+
const dots = document.createElement("span");
|
|
147
|
+
dots.className = "px-1 text-2xs text-ink-400";
|
|
148
|
+
dots.setAttribute("aria-hidden", "true");
|
|
149
|
+
dots.textContent = "...";
|
|
150
|
+
nav.append(dots);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const link = control(String(entry), entry);
|
|
154
|
+
link.setAttribute("aria-label", `Page ${entry}`);
|
|
155
|
+
if (entry === page) {
|
|
156
|
+
link.setAttribute("aria-current", "page");
|
|
157
|
+
link.className = buttonClass("selected", "sm", PAGINATION_EXTRA);
|
|
158
|
+
}
|
|
159
|
+
nav.append(link);
|
|
97
160
|
}
|
|
161
|
+
nav.append(control("Next", page < pageCount ? page + 1 : null, "next"));
|
|
162
|
+
pagination.append(nav);
|
|
98
163
|
}
|
|
99
164
|
}
|
|
100
165
|
}
|
|
@@ -133,6 +198,8 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
133
198
|
stacks: "stacks",
|
|
134
199
|
platforms: "platforms",
|
|
135
200
|
categories: "categories",
|
|
201
|
+
tags: "topics",
|
|
202
|
+
licenses: "licenses",
|
|
136
203
|
};
|
|
137
204
|
const taxonomyName = (groupKey, value) =>
|
|
138
205
|
(taxonomy[taxonomyKinds[groupKey]] || []).find((entry) => entry.id === value)?.name ??
|
|
@@ -152,14 +219,22 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
152
219
|
? `Platform: ${taxonomyName("platforms", chip.value)}`
|
|
153
220
|
: chip.key === "categories"
|
|
154
221
|
? `Category: ${taxonomyName("categories", chip.value)}`
|
|
155
|
-
: chip.
|
|
222
|
+
: chip.key === "tags"
|
|
223
|
+
? `Tag: ${taxonomyName("tags", chip.value)}`
|
|
224
|
+
: chip.key === "licenses"
|
|
225
|
+
? `License: ${taxonomyName("licenses", chip.value)}`
|
|
226
|
+
: chip.label,
|
|
156
227
|
}));
|
|
157
228
|
|
|
158
229
|
for (const chip of chips) {
|
|
159
230
|
const next = searchParamsFromFilters(removeFilter(filters, chip.key, chip.value));
|
|
160
231
|
const link = document.createElement("a");
|
|
161
232
|
link.href = `/${slug}${next.size ? `?${next}` : ""}`;
|
|
162
|
-
|
|
233
|
+
// The whole anchor is the remove control; the visible label
|
|
234
|
+
// announces the filter value, and the aria-label spells out
|
|
235
|
+
// the action for assistive tech.
|
|
236
|
+
link.setAttribute("aria-label", `Remove filter: ${chip.label}`);
|
|
237
|
+
link.className = chipClass();
|
|
163
238
|
link.append(document.createTextNode(`${chip.label} `));
|
|
164
239
|
const close = document.createElement("span");
|
|
165
240
|
close.setAttribute("aria-hidden", "true");
|
|
@@ -172,10 +247,18 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
172
247
|
const clearParams = searchParamsFromFilters({ sort });
|
|
173
248
|
clear.href = `/${slug}${clearParams.size ? `?${clearParams}` : ""}`;
|
|
174
249
|
clear.className = "text-2xs font-medium text-ink-500 hover:text-ink-900";
|
|
250
|
+
clear.setAttribute("aria-label", "Clear all active filters");
|
|
175
251
|
clear.textContent = "Clear all";
|
|
176
252
|
activeFilters.append(clear);
|
|
177
253
|
}
|
|
178
254
|
activeFilters.classList.toggle("hidden", chips.length === 0);
|
|
255
|
+
|
|
256
|
+
// Mobile filter-drawer badge mirrors the live chip count.
|
|
257
|
+
const drawerCount = document.querySelector("[data-drawer-count]");
|
|
258
|
+
if (drawerCount) {
|
|
259
|
+
drawerCount.textContent = String(chips.length);
|
|
260
|
+
drawerCount.classList.toggle("hidden", chips.length === 0);
|
|
261
|
+
}
|
|
179
262
|
}
|
|
180
263
|
|
|
181
264
|
document.querySelectorAll(".grove-filter").forEach((group) => {
|
|
@@ -199,18 +282,39 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
199
282
|
: `${label}: Any`;
|
|
200
283
|
}
|
|
201
284
|
if (triggerButton) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
triggerButton.
|
|
205
|
-
triggerButton.classList.toggle("hover:text-primary-foreground", selected.length > 0);
|
|
206
|
-
triggerButton.classList.toggle("border-border", selected.length === 0);
|
|
207
|
-
triggerButton.classList.toggle("bg-secondary", selected.length === 0);
|
|
208
|
-
triggerButton.classList.toggle("text-foreground", selected.length === 0);
|
|
209
|
-
triggerButton.classList.toggle("hover:bg-accent", selected.length === 0);
|
|
285
|
+
// Same builder the server render used — the class string is
|
|
286
|
+
// reassigned wholesale so the two can never drift.
|
|
287
|
+
triggerButton.className = filterTriggerClass(selected.length > 0);
|
|
210
288
|
}
|
|
211
289
|
group.querySelector(".grove-filter-clear")?.classList.toggle("hidden", selected.length === 0);
|
|
212
290
|
});
|
|
213
291
|
|
|
292
|
+
// Facet counts were rendered at build time from the UNFILTERED
|
|
293
|
+
// record set (the route is prerendered, so the server never sees
|
|
294
|
+
// the live query string). Recompute intersection counts against
|
|
295
|
+
// the current filters and patch each count span in place —
|
|
296
|
+
// never re-render the option lists, which would destroy
|
|
297
|
+
// checkbox, focus, and scroll state.
|
|
298
|
+
const liveFacets: Record<string, { value: string; count: number }[]> = buildFacets(items, {
|
|
299
|
+
filters,
|
|
300
|
+
curatedTagIds: (taxonomy.topics || []).map((topic: { id: string }) => topic.id),
|
|
301
|
+
});
|
|
302
|
+
document.querySelectorAll("[data-facet-count]").forEach((span) => {
|
|
303
|
+
if (!(span instanceof HTMLElement)) return;
|
|
304
|
+
const options = liveFacets[span.dataset.facet ?? ""] ?? [];
|
|
305
|
+
const count = options.find((option) => option.value === span.dataset.value)?.count ?? 0;
|
|
306
|
+
span.textContent = String(count);
|
|
307
|
+
const row = span.closest("label");
|
|
308
|
+
const input = row?.querySelector(".grove-filter-input");
|
|
309
|
+
const checked = input instanceof HTMLInputElement && input.checked;
|
|
310
|
+
// A zero-count option can't narrow anything — dim and disable
|
|
311
|
+
// it unless it's the currently selected value (which must stay
|
|
312
|
+
// interactive so the user can un-select it).
|
|
313
|
+
const inert = count === 0 && !checked;
|
|
314
|
+
row?.classList.toggle("opacity-45", inert);
|
|
315
|
+
if (input instanceof HTMLInputElement) input.disabled = inert;
|
|
316
|
+
});
|
|
317
|
+
|
|
214
318
|
let activeLens = LENSES[0];
|
|
215
319
|
document.querySelectorAll("[data-lens-id]").forEach((node) => {
|
|
216
320
|
if (!(node instanceof HTMLAnchorElement)) return;
|
|
@@ -220,14 +324,7 @@ const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).re
|
|
|
220
324
|
node.href = hrefForLens(lens.id, params, `/${slug}`);
|
|
221
325
|
if (active) node.setAttribute("aria-current", "page");
|
|
222
326
|
else node.removeAttribute("aria-current");
|
|
223
|
-
node.
|
|
224
|
-
node.classList.toggle("text-primary-foreground", active);
|
|
225
|
-
node.classList.toggle("hover:bg-primary/90", active);
|
|
226
|
-
node.classList.toggle("hover:text-primary-foreground", active);
|
|
227
|
-
node.classList.toggle("border-border", !active);
|
|
228
|
-
node.classList.toggle("bg-secondary", !active);
|
|
229
|
-
node.classList.toggle("text-foreground", !active);
|
|
230
|
-
node.classList.toggle("hover:bg-accent", !active);
|
|
327
|
+
node.className = lensTabClass(active);
|
|
231
328
|
if (active) activeLens = lens;
|
|
232
329
|
});
|
|
233
330
|
const heading = document.querySelector("#results-heading");
|
|
@@ -20,16 +20,17 @@ const hasAny = bestFor.length > 0 || caveats.length > 0;
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
{hasAny && (
|
|
23
|
-
<section class="grove-editorial mb-10 rounded-[var(--radius-lg)] border border-ink-200 bg-
|
|
23
|
+
<section class="grove-editorial mb-10 rounded-[var(--radius-lg)] border border-ink-200 bg-card p-5 dark:border-ink-800" aria-label="Editorial summary">
|
|
24
|
+
<h2 class="sr-only">Editorial summary</h2>
|
|
24
25
|
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
25
26
|
{bestFor.length > 0 && (
|
|
26
27
|
<div>
|
|
27
|
-
<
|
|
28
|
+
<h3 class="m-0 flex items-center gap-1.5 text-2xs font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-400">
|
|
28
29
|
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
29
30
|
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
|
30
31
|
</svg>
|
|
31
32
|
Best for
|
|
32
|
-
</
|
|
33
|
+
</h3>
|
|
33
34
|
<ul class="mt-2 m-0 flex list-none flex-col gap-1.5 p-0 text-sm text-ink-700 dark:text-ink-200">
|
|
34
35
|
{bestFor.map((b) => (
|
|
35
36
|
<li class="leading-relaxed">{b}</li>
|
|
@@ -39,12 +40,12 @@ const hasAny = bestFor.length > 0 || caveats.length > 0;
|
|
|
39
40
|
)}
|
|
40
41
|
{caveats.length > 0 && (
|
|
41
42
|
<div>
|
|
42
|
-
<
|
|
43
|
+
<h3 class="m-0 flex items-center gap-1.5 text-2xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
|
43
44
|
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
44
45
|
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm9 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 .4 1.04l1.6.8a.75.75 0 1 0 .67-1.34l-1.17-.59V4.75z" />
|
|
45
46
|
</svg>
|
|
46
47
|
Consider before using
|
|
47
|
-
</
|
|
48
|
+
</h3>
|
|
48
49
|
<ul class="mt-2 m-0 flex list-none flex-col gap-1.5 p-0 text-sm text-ink-700 dark:text-ink-200">
|
|
49
50
|
{caveats.map((c) => (
|
|
50
51
|
<li class="leading-relaxed">{c}</li>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import FilterOptions from "./FilterOptions.astro";
|
|
3
|
+
import { buttonClass, filterTriggerClass } from "../ui/button.js";
|
|
3
4
|
|
|
4
5
|
interface FacetValue {
|
|
5
6
|
value: string;
|
|
@@ -24,13 +25,7 @@ const group = Astro.props;
|
|
|
24
25
|
<div class="grove-filter relative" data-filter-group-key={group.filterKey} data-filter-single={group.single ? "true" : "false"}>
|
|
25
26
|
<button
|
|
26
27
|
type="button"
|
|
27
|
-
class
|
|
28
|
-
"grove-filter-trigger inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-[0.8125rem] font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35",
|
|
29
|
-
group.active
|
|
30
|
-
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
|
|
31
|
-
: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
32
|
-
]}
|
|
33
|
-
aria-haspopup="listbox"
|
|
28
|
+
class={filterTriggerClass(group.active)}
|
|
34
29
|
aria-expanded="false"
|
|
35
30
|
aria-controls={group.popoverId}
|
|
36
31
|
>
|
|
@@ -39,9 +34,26 @@ const group = Astro.props;
|
|
|
39
34
|
<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
35
|
</svg>
|
|
41
36
|
</button>
|
|
42
|
-
|
|
37
|
+
{/* A group of native checkboxes/radios — the old listbox role over
|
|
38
|
+
label/input children had no valid accessibility tree. */}
|
|
39
|
+
<div
|
|
40
|
+
id={group.popoverId}
|
|
41
|
+
class="rounded-[calc(var(--radius)+0.25rem)] border border-border bg-popover text-popover-foreground shadow-xl grove-filter-popover absolute left-0 top-full z-40 mt-1 hidden w-72 max-w-[calc(100vw-2rem)] p-1.5"
|
|
42
|
+
role="group"
|
|
43
|
+
aria-label={`${group.label} filters`}
|
|
44
|
+
hidden
|
|
45
|
+
>
|
|
43
46
|
<FilterOptions filterKey={group.filterKey} options={group.options} initial={group.initial} single={group.single} />
|
|
44
|
-
|
|
47
|
+
{/* Single-select groups navigate on change, so Apply would be a
|
|
48
|
+
dead affordance — they keep only Clear, and the whole footer
|
|
49
|
+
hides itself while Clear is hidden (the client script toggles
|
|
50
|
+
Clear's `hidden` class from the live URL state). */}
|
|
51
|
+
<div
|
|
52
|
+
class:list={[
|
|
53
|
+
"mt-1 flex items-center justify-between border-t border-ink-100 px-2 pt-1.5 dark:border-ink-900",
|
|
54
|
+
group.single && "has-[.grove-filter-clear.hidden]:hidden",
|
|
55
|
+
]}
|
|
56
|
+
>
|
|
45
57
|
<button
|
|
46
58
|
type="button"
|
|
47
59
|
class:list={[
|
|
@@ -49,7 +61,9 @@ const group = Astro.props;
|
|
|
49
61
|
!group.active && "hidden",
|
|
50
62
|
]}
|
|
51
63
|
>Clear {group.label.toLowerCase()}</button>
|
|
52
|
-
|
|
64
|
+
{!group.single && (
|
|
65
|
+
<button type="button" class={buttonClass("primary", "sm", "grove-filter-apply ml-auto px-2.5 text-2xs")}>Apply</button>
|
|
66
|
+
)}
|
|
53
67
|
</div>
|
|
54
68
|
</div>
|
|
55
69
|
</div>
|
|
@@ -37,7 +37,12 @@ function pretty(value: string): string {
|
|
|
37
37
|
/>
|
|
38
38
|
<span class="truncate">{option.label ?? pretty(option.value)}</span>
|
|
39
39
|
</span>
|
|
40
|
-
<span
|
|
40
|
+
<span
|
|
41
|
+
class="font-mono text-2xs tabular-nums text-ink-500 dark:text-ink-400"
|
|
42
|
+
data-facet-count
|
|
43
|
+
data-facet={filterKey}
|
|
44
|
+
data-value={option.value}
|
|
45
|
+
>{option.count ?? 0}</span>
|
|
41
46
|
</label>
|
|
42
47
|
</li>
|
|
43
48
|
))}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { buttonClass } from "../ui/button.js";
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
itemLabel?: string;
|
|
4
6
|
itemsLabel?: string;
|
|
@@ -18,7 +20,7 @@ const {
|
|
|
18
20
|
|
|
19
21
|
<section class:list={["border-t border-ink-200 py-10 sm:py-12 dark:border-ink-800", className]} aria-labelledby="grove-final-cta-heading">
|
|
20
22
|
<div class="mx-auto w-full max-w-container px-5 sm:px-7">
|
|
21
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border border-
|
|
23
|
+
<div class="rounded-[calc(var(--radius)+0.25rem)] border border-border bg-card px-6 py-7 sm:flex sm:items-center sm:justify-between sm:gap-10 sm:px-8 sm:py-8 dark:border-ink-800">
|
|
22
24
|
<div class="max-w-2xl">
|
|
23
25
|
<p class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Keep the directory useful</p>
|
|
24
26
|
<h2 id="grove-final-cta-heading" class="m-0 mt-1.5 text-[1.5rem] font-semibold tracking-tight text-ink-900 sm:text-[1.75rem] dark:text-ink-100">
|
|
@@ -29,10 +31,10 @@ const {
|
|
|
29
31
|
</p>
|
|
30
32
|
</div>
|
|
31
33
|
<div class="mt-5 flex shrink-0 flex-wrap items-center gap-2 sm:mt-0 sm:justify-end">
|
|
32
|
-
<a href={submitHref} class="
|
|
34
|
+
<a href={submitHref} class={buttonClass("primary", "md", "px-4")}>
|
|
33
35
|
Submit {itemLabel}
|
|
34
36
|
</a>
|
|
35
|
-
<a href={browseHref} class="
|
|
37
|
+
<a href={browseHref} class={buttonClass("outline", "md", "px-4")}>
|
|
36
38
|
Explore {itemsLabel}
|
|
37
39
|
</a>
|
|
38
40
|
</div>
|