@grove-dev/astro 0.2.20 → 0.3.2
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/LICENSE +21 -0
- package/README.md +9 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -26
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +1 -103
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +1 -28
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +1 -71
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -57
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +1 -198
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +1 -19
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +1 -30
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +1 -36
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +1 -75
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +1 -113
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +1 -379
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -10
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +1 -21
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +22 -10
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +2 -2
- package/src/components/DirectoryIndexClient.astro +240 -0
- package/src/components/FilterGroupMenu.astro +8 -4
- package/src/components/FinalCta.astro +41 -0
- package/src/components/Hero.astro +15 -18
- package/src/components/Icon.astro +99 -27
- package/src/components/RefinePanel.astro +7 -7
- package/src/components/SmartLensTabs.astro +5 -4
- package/src/components/StackGrid.astro +1 -1
- package/src/components/SubmissionClient.astro +133 -0
- package/src/index.ts +11 -1
- package/src/layouts/BaseLayout.astro +19 -1
- package/src/layouts/Footer.astro +25 -56
- package/src/layouts/Header.astro +33 -15
- package/src/layouts/ThemeToggle.astro +7 -3
- package/src/lib/display.ts +1 -111
- package/src/lib/format.ts +1 -64
- package/src/lib/lenses.ts +1 -220
- package/src/lib/repo.ts +1 -32
- package/src/lib/scores.ts +1 -90
- package/src/lib/search.ts +1 -418
- package/src/lib/taxonomy-counts.ts +1 -26
- package/{templates/default/src/data/records.ts → src/server/directory.ts} +90 -8
- package/src/server/index.ts +2 -0
- package/src/server/models.ts +432 -0
- package/dist/theme.test.d.ts +0 -2
- package/dist/theme.test.d.ts.map +0 -1
- package/dist/theme.test.js +0 -55
- package/dist/theme.test.js.map +0 -1
- package/src/theme.test.ts +0 -67
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
- package/templates/default/.github/pull_request_template.md +0 -27
- package/templates/default/.github/workflows/build.yml +0 -55
- package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -48
- package/templates/default/.github/workflows/daily-refresh.yml +0 -53
- package/templates/default/.github/workflows/sync-contributors.yml +0 -53
- package/templates/default/.github/workflows/sync-github-metadata.yml +0 -58
- package/templates/default/.github/workflows/validate-data.yml +0 -35
- package/templates/default/astro.config.mjs +0 -25
- package/templates/default/content/pages/about.md +0 -66
- package/templates/default/content/pages/methodology.md +0 -103
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/data/records/coolify.yml +0 -46
- package/templates/default/data/records/gitea.yml +0 -46
- package/templates/default/data/records/hoppscotch.yml +0 -47
- package/templates/default/data/records/inventree.yml +0 -47
- package/templates/default/data/records/label-studio.yml +0 -46
- package/templates/default/data/records/logseq.yml +0 -48
- package/templates/default/data/records/private-gpt.yml +0 -47
- package/templates/default/data/records/wakapi.yml +0 -44
- package/templates/default/data/taxonomy/categories.yml +0 -47
- package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
- package/templates/default/data/taxonomy/platforms.yml +0 -27
- package/templates/default/data/taxonomy/stacks.yml +0 -41
- package/templates/default/grove.config.ts +0 -36
- package/templates/default/package.json +0 -38
- package/templates/default/public/icons/platforms/android.svg +0 -1
- package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
- package/templates/default/public/icons/platforms/apple-light.svg +0 -1
- package/templates/default/public/icons/platforms/chrome.svg +0 -1
- package/templates/default/public/icons/platforms/chromeos.svg +0 -5
- package/templates/default/public/icons/platforms/desktop.svg +0 -4
- package/templates/default/public/icons/platforms/embedded.svg +0 -5
- package/templates/default/public/icons/platforms/ios.svg +0 -1
- package/templates/default/public/icons/platforms/linux.svg +0 -1
- package/templates/default/public/icons/platforms/macos.svg +0 -1
- package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
- package/templates/default/public/icons/platforms/web.svg +0 -5
- package/templates/default/public/icons/platforms/windows.svg +0 -6
- package/templates/default/public/icons/stacks/android.svg +0 -1
- package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
- package/templates/default/public/icons/stacks/apple-light.svg +0 -1
- package/templates/default/public/icons/stacks/bun.svg +0 -1
- package/templates/default/public/icons/stacks/capacitor.svg +0 -14
- package/templates/default/public/icons/stacks/clojure.svg +0 -1
- package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
- package/templates/default/public/icons/stacks/dart.svg +0 -1
- package/templates/default/public/icons/stacks/deno.svg +0 -1
- package/templates/default/public/icons/stacks/django.svg +0 -1
- package/templates/default/public/icons/stacks/docker.svg +0 -1
- package/templates/default/public/icons/stacks/firebase.svg +0 -1
- package/templates/default/public/icons/stacks/flutter.svg +0 -1
- package/templates/default/public/icons/stacks/go.svg +0 -1
- package/templates/default/public/icons/stacks/graphql.svg +0 -1
- package/templates/default/public/icons/stacks/ionic.svg +0 -4
- package/templates/default/public/icons/stacks/java.svg +0 -1
- package/templates/default/public/icons/stacks/javascript.svg +0 -1
- package/templates/default/public/icons/stacks/kotlin.svg +0 -1
- package/templates/default/public/icons/stacks/llm.svg +0 -1
- package/templates/default/public/icons/stacks/mongodb.svg +0 -1
- package/templates/default/public/icons/stacks/node.js.svg +0 -1
- package/templates/default/public/icons/stacks/nodejs.svg +0 -1
- package/templates/default/public/icons/stacks/python.svg +0 -1
- package/templates/default/public/icons/stacks/rag.svg +0 -7
- package/templates/default/public/icons/stacks/react-native.svg +0 -1
- package/templates/default/public/icons/stacks/react.svg +0 -1
- package/templates/default/public/icons/stacks/rust.svg +0 -1
- package/templates/default/public/icons/stacks/solidity.svg +0 -1
- package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
- package/templates/default/public/icons/stacks/swift.svg +0 -1
- package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
- package/templates/default/public/icons/stacks/typescript.svg +0 -1
- package/templates/default/public/icons/stacks/vue.svg +0 -1
- package/templates/default/public/llms-full.txt +0 -206
- package/templates/default/public/llms.txt +0 -112
- package/templates/default/public/og-image.svg +0 -48
- package/templates/default/public/robots.txt +0 -53
- package/templates/default/public/sitemap.xml +0 -63
- package/templates/default/scripts/build-llms.mjs +0 -123
- package/templates/default/scripts/build-records-json.mjs +0 -27
- package/templates/default/scripts/build-sitemap.mjs +0 -20
- package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
- package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
- package/templates/default/scripts/fetch-icons.mjs +0 -137
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
- package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
- package/templates/default/scripts/refresh-records-activity.mjs +0 -24
- package/templates/default/scripts/repair-license-format.mjs +0 -72
- package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
- package/templates/default/scripts/seed-from-github.mjs +0 -62
- package/templates/default/scripts/sync-contributors.mjs +0 -145
- package/templates/default/scripts/sync-github-metadata.mjs +0 -28
- package/templates/default/scripts/validate-records.mjs +0 -27
- package/templates/default/src/pages/404.astro +0 -64
- package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -689
- package/templates/default/src/pages/[slug]/index.astro +0 -278
- package/templates/default/src/pages/about.astro +0 -182
- package/templates/default/src/pages/apps/[recordSlug].astro +0 -33
- package/templates/default/src/pages/contributors.astro +0 -175
- package/templates/default/src/pages/index.astro +0 -225
- package/templates/default/src/pages/sitemap.xml.ts +0 -89
- package/templates/default/src/pages/submit.astro +0 -281
- package/templates/default/src/styles/global.css +0 -33
- package/templates/default/wrangler.jsonc +0 -28
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
clientItemsJson: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
singular: string;
|
|
6
|
+
plural: string;
|
|
7
|
+
taxonomy: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { clientItemsJson, slug, singular, plural, taxonomy } = Astro.props;
|
|
11
|
+
const clientConfigJson = JSON.stringify({ slug, singular, plural, taxonomy }).replace(/</g, "\\u003c");
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<script is:inline id="grove-index-data" type="application/json" set:html={clientItemsJson}></script>
|
|
15
|
+
<script is:inline id="grove-index-config" type="application/json" set:html={clientConfigJson}></script>
|
|
16
|
+
<script>
|
|
17
|
+
import {
|
|
18
|
+
LENSES,
|
|
19
|
+
PAGE_SIZE,
|
|
20
|
+
activeFilterChips,
|
|
21
|
+
applySort,
|
|
22
|
+
effectivePage,
|
|
23
|
+
effectiveSort,
|
|
24
|
+
filterRecords,
|
|
25
|
+
filtersFromSearchParams,
|
|
26
|
+
hrefForLens,
|
|
27
|
+
isLensActive,
|
|
28
|
+
removeFilter,
|
|
29
|
+
searchParamsFromFilters,
|
|
30
|
+
sortDisplay,
|
|
31
|
+
totalPages,
|
|
32
|
+
} from "@grove-dev/core/directory";
|
|
33
|
+
|
|
34
|
+
(() => {
|
|
35
|
+
const source = document.querySelector("#grove-index-data");
|
|
36
|
+
const configSource = document.querySelector("#grove-index-config");
|
|
37
|
+
const list = document.querySelector("#results-list");
|
|
38
|
+
if (!source || !configSource || !list) return;
|
|
39
|
+
|
|
40
|
+
const items = JSON.parse(source.textContent || "[]");
|
|
41
|
+
const { slug, singular, plural, taxonomy } = JSON.parse(configSource.textContent || "{}");
|
|
42
|
+
const nodes = new Map(
|
|
43
|
+
[...list.querySelectorAll("[data-record-slug]")].map((node) => [
|
|
44
|
+
(node as HTMLElement).dataset.recordSlug,
|
|
45
|
+
node,
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
const params = new URLSearchParams(location.search);
|
|
49
|
+
const values = (key) => params.getAll(key).filter(Boolean);
|
|
50
|
+
const stacks = values("stack");
|
|
51
|
+
const platforms = values("platform");
|
|
52
|
+
const categories = values("category");
|
|
53
|
+
const filters = filtersFromSearchParams(params);
|
|
54
|
+
const sort = effectiveSort(filters);
|
|
55
|
+
const requestedPage = effectivePage(filters);
|
|
56
|
+
|
|
57
|
+
function applyClientFilters() {
|
|
58
|
+
const filtered = applySort(filterRecords(items, filters), sort);
|
|
59
|
+
const pageCount = totalPages(filtered.length);
|
|
60
|
+
const page = Math.min(requestedPage, pageCount);
|
|
61
|
+
const visible = new Set(
|
|
62
|
+
filtered
|
|
63
|
+
.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
|
64
|
+
.map((record) => record.slug),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
for (const record of filtered) {
|
|
68
|
+
const node = nodes.get(record.slug) as HTMLElement | undefined;
|
|
69
|
+
if (node) list.append(node);
|
|
70
|
+
}
|
|
71
|
+
for (const [recordSlug, node] of nodes) {
|
|
72
|
+
(node as HTMLElement).hidden = !visible.has(recordSlug);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const count = document.querySelector("#results-count");
|
|
76
|
+
const pageLabel = document.querySelector("#results-page");
|
|
77
|
+
const empty = document.querySelector("#empty-state");
|
|
78
|
+
if (count) count.textContent = `${filtered.length} ${filtered.length === 1 ? singular : plural}`;
|
|
79
|
+
const sortLabel = document.querySelector("#results-sort");
|
|
80
|
+
if (sortLabel) sortLabel.textContent = ` · sorted by ${sortDisplay(sort)}`;
|
|
81
|
+
if (pageLabel) pageLabel.textContent = pageCount > 1 ? ` · page ${page} of ${pageCount}` : "";
|
|
82
|
+
if (empty) empty.classList.toggle("hidden", filtered.length > 0);
|
|
83
|
+
|
|
84
|
+
const pagination = document.querySelector("#results-pagination");
|
|
85
|
+
if (pagination) {
|
|
86
|
+
pagination.innerHTML = "";
|
|
87
|
+
if (pageCount > 1) {
|
|
88
|
+
for (let target = 1; target <= pageCount; target += 1) {
|
|
89
|
+
const next = searchParamsFromFilters({ ...filters, page: target });
|
|
90
|
+
const link = document.createElement("a");
|
|
91
|
+
link.href = `/${slug}${next.size ? `?${next}` : ""}`;
|
|
92
|
+
link.textContent = String(target);
|
|
93
|
+
link.setAttribute("aria-label", `Page ${target}`);
|
|
94
|
+
if (target === page) link.setAttribute("aria-current", "page");
|
|
95
|
+
link.className = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border px-2 text-sm";
|
|
96
|
+
pagination.append(link);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const searchInput = document.querySelector("#search-input");
|
|
103
|
+
if (searchInput instanceof HTMLInputElement) searchInput.value = filters.q || "";
|
|
104
|
+
document.querySelectorAll('select[name="sort"]').forEach((select) => {
|
|
105
|
+
if (select instanceof HTMLSelectElement) select.value = sort;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
document.querySelector("#search-form")?.addEventListener("submit", (event) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
const next = new URLSearchParams(params);
|
|
111
|
+
const query = searchInput instanceof HTMLInputElement ? searchInput.value.trim() : "";
|
|
112
|
+
if (query) next.set("q", query);
|
|
113
|
+
else next.delete("q");
|
|
114
|
+
next.set("sort", sort);
|
|
115
|
+
next.delete("page");
|
|
116
|
+
location.assign(`/${slug}${next.size ? `?${next}` : ""}`);
|
|
117
|
+
});
|
|
118
|
+
const filterKeys = {
|
|
119
|
+
stacks: "stack",
|
|
120
|
+
platforms: "platform",
|
|
121
|
+
categories: "category",
|
|
122
|
+
tags: "tag",
|
|
123
|
+
licenses: "license",
|
|
124
|
+
};
|
|
125
|
+
const filterLabels = {
|
|
126
|
+
stacks: "Stack",
|
|
127
|
+
platforms: "Platform",
|
|
128
|
+
categories: "Category",
|
|
129
|
+
tags: "Tag",
|
|
130
|
+
licenses: "License",
|
|
131
|
+
};
|
|
132
|
+
const taxonomyKinds = {
|
|
133
|
+
stacks: "stacks",
|
|
134
|
+
platforms: "platforms",
|
|
135
|
+
categories: "categories",
|
|
136
|
+
};
|
|
137
|
+
const taxonomyName = (groupKey, value) =>
|
|
138
|
+
(taxonomy[taxonomyKinds[groupKey]] || []).find((entry) => entry.id === value)?.name ??
|
|
139
|
+
value;
|
|
140
|
+
|
|
141
|
+
// This route is prerendered, so Astro cannot see the browser's query
|
|
142
|
+
// string. Rebuild active-filter controls from the live URL instead of
|
|
143
|
+
// leaving the server-rendered (empty) state in place.
|
|
144
|
+
const activeFilters = document.querySelector("#active-filters");
|
|
145
|
+
if (activeFilters) {
|
|
146
|
+
activeFilters.innerHTML = "";
|
|
147
|
+
const chips = activeFilterChips(filters).map((chip) => ({
|
|
148
|
+
...chip,
|
|
149
|
+
label: chip.key === "stacks"
|
|
150
|
+
? `Stack: ${taxonomyName("stacks", chip.value)}`
|
|
151
|
+
: chip.key === "platforms"
|
|
152
|
+
? `Platform: ${taxonomyName("platforms", chip.value)}`
|
|
153
|
+
: chip.key === "categories"
|
|
154
|
+
? `Category: ${taxonomyName("categories", chip.value)}`
|
|
155
|
+
: chip.label,
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
for (const chip of chips) {
|
|
159
|
+
const next = searchParamsFromFilters(removeFilter(filters, chip.key, chip.value));
|
|
160
|
+
const link = document.createElement("a");
|
|
161
|
+
link.href = `/${slug}${next.size ? `?${next}` : ""}`;
|
|
162
|
+
link.className = "inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground transition-colors hover:border-ink-400";
|
|
163
|
+
link.append(document.createTextNode(`${chip.label} `));
|
|
164
|
+
const close = document.createElement("span");
|
|
165
|
+
close.setAttribute("aria-hidden", "true");
|
|
166
|
+
close.textContent = "x";
|
|
167
|
+
link.append(close);
|
|
168
|
+
activeFilters.append(link);
|
|
169
|
+
}
|
|
170
|
+
if (chips.length) {
|
|
171
|
+
const clear = document.createElement("a");
|
|
172
|
+
const clearParams = searchParamsFromFilters({ sort });
|
|
173
|
+
clear.href = `/${slug}${clearParams.size ? `?${clearParams}` : ""}`;
|
|
174
|
+
clear.className = "text-2xs font-medium text-ink-500 hover:text-ink-900";
|
|
175
|
+
clear.textContent = "Clear all";
|
|
176
|
+
activeFilters.append(clear);
|
|
177
|
+
}
|
|
178
|
+
activeFilters.classList.toggle("hidden", chips.length === 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.querySelectorAll(".grove-filter").forEach((group) => {
|
|
182
|
+
if (!(group instanceof HTMLElement)) return;
|
|
183
|
+
const selected = values(filterKeys[group.dataset.filterGroupKey]);
|
|
184
|
+
group.querySelectorAll(".grove-filter-input").forEach((input) => {
|
|
185
|
+
if (input instanceof HTMLInputElement) {
|
|
186
|
+
input.checked = selected.includes(input.dataset.filterValue || "");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const triggerButton = group.querySelector(".grove-filter-trigger");
|
|
190
|
+
const trigger = triggerButton?.querySelector("span");
|
|
191
|
+
const label = filterLabels[group.dataset.filterGroupKey];
|
|
192
|
+
if (trigger) {
|
|
193
|
+
trigger.textContent = selected.length
|
|
194
|
+
? `${label}: ${
|
|
195
|
+
selected.length === 1
|
|
196
|
+
? taxonomyName(group.dataset.filterGroupKey, selected[0])
|
|
197
|
+
: `${selected.length} selected`
|
|
198
|
+
}`
|
|
199
|
+
: `${label}: Any`;
|
|
200
|
+
}
|
|
201
|
+
if (triggerButton) {
|
|
202
|
+
triggerButton.classList.toggle("bg-primary", selected.length > 0);
|
|
203
|
+
triggerButton.classList.toggle("text-primary-foreground", selected.length > 0);
|
|
204
|
+
triggerButton.classList.toggle("hover:bg-primary/90", selected.length > 0);
|
|
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);
|
|
210
|
+
}
|
|
211
|
+
group.querySelector(".grove-filter-clear")?.classList.toggle("hidden", selected.length === 0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
let activeLens = LENSES[0];
|
|
215
|
+
document.querySelectorAll("[data-lens-id]").forEach((node) => {
|
|
216
|
+
if (!(node instanceof HTMLAnchorElement)) return;
|
|
217
|
+
const lens = LENSES.find((candidate) => candidate.id === node.dataset.lensId);
|
|
218
|
+
if (!lens) return;
|
|
219
|
+
const active = isLensActive(lens.id, params);
|
|
220
|
+
node.href = hrefForLens(lens.id, params, `/${slug}`);
|
|
221
|
+
if (active) node.setAttribute("aria-current", "page");
|
|
222
|
+
else node.removeAttribute("aria-current");
|
|
223
|
+
node.classList.toggle("bg-primary", active);
|
|
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);
|
|
231
|
+
if (active) activeLens = lens;
|
|
232
|
+
});
|
|
233
|
+
const heading = document.querySelector("#results-heading");
|
|
234
|
+
const description = document.querySelector("#results-description");
|
|
235
|
+
if (heading) heading.textContent = activeLens.id === "all" ? `All ${plural}` : activeLens.label;
|
|
236
|
+
if (description) description.textContent = activeLens.description || `Every ${singular} in the directory.`;
|
|
237
|
+
|
|
238
|
+
applyClientFilters();
|
|
239
|
+
})();
|
|
240
|
+
</script>
|
|
@@ -27,7 +27,7 @@ const group = Astro.props;
|
|
|
27
27
|
class:list={[
|
|
28
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
29
|
group.active
|
|
30
|
-
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
30
|
+
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
|
|
31
31
|
: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
32
32
|
]}
|
|
33
33
|
aria-haspopup="listbox"
|
|
@@ -42,9 +42,13 @@ const group = Astro.props;
|
|
|
42
42
|
<div id={group.popoverId} 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" role="listbox" hidden>
|
|
43
43
|
<FilterOptions filterKey={group.filterKey} options={group.options} initial={group.initial} single={group.single} />
|
|
44
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class:list={[
|
|
48
|
+
"grove-filter-clear text-2xs font-medium text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100",
|
|
49
|
+
!group.active && "hidden",
|
|
50
|
+
]}
|
|
51
|
+
>Clear {group.label.toLowerCase()}</button>
|
|
48
52
|
<button type="button" class="grove-filter-apply inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90 ml-auto px-2.5 text-2xs">Apply</button>
|
|
49
53
|
</div>
|
|
50
54
|
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
itemLabel?: string;
|
|
4
|
+
itemsLabel?: string;
|
|
5
|
+
browseHref?: string;
|
|
6
|
+
submitHref?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
itemLabel = "item",
|
|
12
|
+
itemsLabel = "items",
|
|
13
|
+
browseHref = "/items",
|
|
14
|
+
submitHref = "/submit",
|
|
15
|
+
class: className = "",
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<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
|
+
<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-ink-200 bg-muted 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
|
+
<div class="max-w-2xl">
|
|
23
|
+
<p class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Keep the directory useful</p>
|
|
24
|
+
<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">
|
|
25
|
+
Know {itemLabel === "app" ? "an" : "a"} {itemLabel} that belongs here?
|
|
26
|
+
</h2>
|
|
27
|
+
<p class="m-0 mt-2 max-w-xl text-sm leading-relaxed text-ink-500 dark:text-ink-400">
|
|
28
|
+
Add a real, inspectable project or improve the metadata behind an existing listing.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="mt-5 flex shrink-0 flex-wrap items-center gap-2 sm:mt-0 sm:justify-end">
|
|
32
|
+
<a href={submitHref} class="inline-flex min-h-9 items-center justify-center rounded-[var(--radius)] bg-primary px-4 text-sm font-semibold text-primary-foreground no-underline hover:bg-primary/90">
|
|
33
|
+
Submit {itemLabel}
|
|
34
|
+
</a>
|
|
35
|
+
<a href={browseHref} class="inline-flex min-h-9 items-center justify-center rounded-[var(--radius)] border border-border bg-background px-4 text-sm font-semibold text-foreground no-underline hover:bg-accent">
|
|
36
|
+
Explore {itemsLabel}
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
@@ -37,6 +37,7 @@ interface Props {
|
|
|
37
37
|
totalStacks?: number;
|
|
38
38
|
totalOwners?: number;
|
|
39
39
|
totalStars?: number;
|
|
40
|
+
repositoryContributors?: number;
|
|
40
41
|
originalRepo?: string;
|
|
41
42
|
};
|
|
42
43
|
/** Search form target — the list page. */
|
|
@@ -94,11 +95,11 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
94
95
|
...(stats.totalStacks !== undefined
|
|
95
96
|
? [{ value: String(stats.totalStacks), label: "stacks" }]
|
|
96
97
|
: []),
|
|
97
|
-
...(stats.totalOwners !== undefined
|
|
98
|
-
? [{ value: compact(stats.totalOwners) ?? "0", label: "
|
|
98
|
+
...(stats.totalOwners !== undefined && stats.totalOwners > 0
|
|
99
|
+
? [{ value: compact(stats.totalOwners) ?? "0", label: "project owners" }]
|
|
99
100
|
: []),
|
|
100
|
-
...(stats.totalStars !== undefined
|
|
101
|
-
? [{ value: compact(stats.totalStars) ?? "0", label: "
|
|
101
|
+
...(stats.totalStars !== undefined && stats.totalStars > 0
|
|
102
|
+
? [{ value: compact(stats.totalStars) ?? "0", label: "stars across projects" }]
|
|
102
103
|
: []),
|
|
103
104
|
];
|
|
104
105
|
---
|
|
@@ -121,7 +122,7 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
121
122
|
></div>
|
|
122
123
|
|
|
123
124
|
<Container>
|
|
124
|
-
<div class="relative py-
|
|
125
|
+
<div class="relative py-12 sm:py-16">
|
|
125
126
|
<div class="max-w-3xl">
|
|
126
127
|
<span
|
|
127
128
|
class="inline-flex animate-fade-in items-center gap-2 text-2xs font-medium text-ink-500 dark:text-ink-400"
|
|
@@ -167,7 +168,13 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
167
168
|
{/* Trust stats row */}
|
|
168
169
|
{trustStats.length > 0 && (
|
|
169
170
|
<dl
|
|
170
|
-
class=
|
|
171
|
+
class:list={[
|
|
172
|
+
"mt-7 grid max-w-3xl grid-cols-2 gap-x-6 gap-y-3",
|
|
173
|
+
trustStats.length === 2 && "sm:grid-cols-2",
|
|
174
|
+
trustStats.length === 3 && "grid-cols-3",
|
|
175
|
+
trustStats.length === 4 && "sm:grid-cols-4",
|
|
176
|
+
trustStats.length >= 5 && "sm:grid-cols-5",
|
|
177
|
+
]}
|
|
171
178
|
aria-label="Directory at a glance"
|
|
172
179
|
>
|
|
173
180
|
{trustStats.map((s) => (
|
|
@@ -184,9 +191,9 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
184
191
|
</dl>
|
|
185
192
|
)}
|
|
186
193
|
|
|
187
|
-
{/* Search
|
|
194
|
+
{/* Search */}
|
|
188
195
|
<form
|
|
189
|
-
class="mt-7 flex flex-col gap-2 sm:flex-row sm:items-stretch"
|
|
196
|
+
class="mt-7 flex max-w-4xl flex-col gap-2 sm:flex-row sm:items-stretch"
|
|
190
197
|
role="search"
|
|
191
198
|
action={searchHref}
|
|
192
199
|
method="get"
|
|
@@ -212,16 +219,6 @@ const trustStats: { value: string; label: string }[] = [
|
|
|
212
219
|
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"
|
|
213
220
|
/>
|
|
214
221
|
</label>
|
|
215
|
-
<a
|
|
216
|
-
href={searchHref}
|
|
217
|
-
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 border-border bg-secondary text-foreground hover:bg-accent h-10"
|
|
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
222
|
<button
|
|
226
223
|
type="submit"
|
|
227
224
|
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 sm:px-5"
|
|
@@ -62,6 +62,11 @@ interface Props {
|
|
|
62
62
|
* {name}.svg` (case-insensitive).
|
|
63
63
|
*/
|
|
64
64
|
aliases?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Asset ids supplied by a consumer in addition to Grove's bundled registry.
|
|
67
|
+
* Unknown stack/platform ids render initials without issuing a failed request.
|
|
68
|
+
*/
|
|
69
|
+
available?: string[];
|
|
65
70
|
/**
|
|
66
71
|
* Initials to render when the icon file is missing. Defaults to
|
|
67
72
|
* the first 1–2 characters of `name`.
|
|
@@ -82,6 +87,7 @@ const {
|
|
|
82
87
|
title,
|
|
83
88
|
mode = "static",
|
|
84
89
|
aliases,
|
|
90
|
+
available,
|
|
85
91
|
fallbackInitials,
|
|
86
92
|
base = "/icons",
|
|
87
93
|
} = Astro.props;
|
|
@@ -94,9 +100,23 @@ function slugify(s: string): string {
|
|
|
94
100
|
// Resolve `name` through the aliases map.
|
|
95
101
|
function resolveName(raw: string): string {
|
|
96
102
|
if (!raw) return "";
|
|
97
|
-
|
|
103
|
+
const defaultAliases: Partial<Record<IconCategory, Record<string, string>>> = {
|
|
104
|
+
stack: {
|
|
105
|
+
ios: "apple",
|
|
106
|
+
"native-ios": "apple",
|
|
107
|
+
ipados: "apple",
|
|
108
|
+
macos: "apple",
|
|
109
|
+
"objective-c": "apple",
|
|
110
|
+
spritekit: "apple",
|
|
111
|
+
swiftui: "apple",
|
|
112
|
+
kmp: "kotlin",
|
|
113
|
+
"kotlin-multiplatform": "kotlin",
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
for (const registry of [aliases, defaultAliases[category]]) {
|
|
117
|
+
if (!registry) continue;
|
|
98
118
|
const lower = raw.toLowerCase();
|
|
99
|
-
for (const [k, v] of Object.entries(
|
|
119
|
+
for (const [k, v] of Object.entries(registry)) {
|
|
100
120
|
if (k.toLowerCase() === lower) return slugify(v);
|
|
101
121
|
}
|
|
102
122
|
}
|
|
@@ -104,6 +124,26 @@ function resolveName(raw: string): string {
|
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
const resolved = resolveName(name);
|
|
127
|
+
const bundledIcons: Record<"stack" | "platform", Set<string>> = {
|
|
128
|
+
stack: new Set([
|
|
129
|
+
"android", "apple", "bun", "capacitor", "clojure", "clojurescript",
|
|
130
|
+
"dart", "deno", "django", "docker", "firebase", "flutter", "go",
|
|
131
|
+
"graphql", "ionic", "java", "javascript", "kotlin", "llm", "mongodb",
|
|
132
|
+
"node.js", "nodejs", "python", "rag", "react", "react-native", "rust",
|
|
133
|
+
"solidity", "sveltekit", "swift", "tensorflow", "typescript", "vue",
|
|
134
|
+
]),
|
|
135
|
+
platform: new Set([
|
|
136
|
+
"android", "apple", "chrome", "chromeos", "desktop", "embedded", "ios",
|
|
137
|
+
"linux", "macos", "ubuntu", "web", "windows",
|
|
138
|
+
]),
|
|
139
|
+
};
|
|
140
|
+
const availableIcons = available?.map(slugify);
|
|
141
|
+
const hasAsset =
|
|
142
|
+
category === "brand" ||
|
|
143
|
+
(availableIcons
|
|
144
|
+
? availableIcons.includes(resolved)
|
|
145
|
+
: bundledIcons[category].has(resolved));
|
|
146
|
+
const effectiveMode: IconMode = resolved === "apple" ? "auto" : mode;
|
|
107
147
|
const platformStackIcons = new Set(["docker"]);
|
|
108
148
|
const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
|
|
109
149
|
const folder =
|
|
@@ -123,7 +163,9 @@ const init = (fallbackInitials ?? name ?? "")
|
|
|
123
163
|
.slice(0, 2)
|
|
124
164
|
.toUpperCase();
|
|
125
165
|
const showFallback =
|
|
126
|
-
!resolved ||
|
|
166
|
+
!resolved ||
|
|
167
|
+
!hasAsset ||
|
|
168
|
+
(category === "platform" && textOnlyPlatforms.has(resolved));
|
|
127
169
|
|
|
128
170
|
// Build aria-label.
|
|
129
171
|
const ariaLabel = title ?? name;
|
|
@@ -144,34 +186,64 @@ const a11y: Record<string, string> = title
|
|
|
144
186
|
>
|
|
145
187
|
{init || "·"}
|
|
146
188
|
</span>
|
|
147
|
-
) :
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
) : effectiveMode === "auto" ? (
|
|
190
|
+
<>
|
|
191
|
+
<img
|
|
192
|
+
src={`${baseUrl}-light.svg`}
|
|
193
|
+
data-grove-icon
|
|
194
|
+
data-grove-icon-light={`${baseUrl}-light.svg`}
|
|
195
|
+
data-grove-icon-dark={`${baseUrl}-dark.svg`}
|
|
196
|
+
data-grove-icon-fallback={`${baseUrl}-light.svg`}
|
|
197
|
+
width={size}
|
|
198
|
+
height={size}
|
|
199
|
+
alt=""
|
|
200
|
+
class:list={["inline-block shrink-0 align-middle", className]}
|
|
201
|
+
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
|
202
|
+
{...a11y}
|
|
203
|
+
/>
|
|
204
|
+
<span
|
|
205
|
+
hidden
|
|
206
|
+
data-grove-icon-fallback-initials
|
|
207
|
+
class:list={[
|
|
208
|
+
"inline-flex shrink-0 select-none items-center justify-center rounded-sm bg-ink-100 align-middle font-semibold uppercase leading-none text-ink-700 dark:bg-ink-800 dark:text-ink-200",
|
|
209
|
+
className,
|
|
210
|
+
]}
|
|
211
|
+
style={`width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`}
|
|
212
|
+
{...a11y}
|
|
213
|
+
>
|
|
214
|
+
{init || "·"}
|
|
215
|
+
</span>
|
|
216
|
+
</>
|
|
160
217
|
) : (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
<>
|
|
219
|
+
<img
|
|
220
|
+
src={staticSrc}
|
|
221
|
+
width={size}
|
|
222
|
+
height={size}
|
|
223
|
+
alt=""
|
|
224
|
+
class:list={["inline-block shrink-0 align-middle", className]}
|
|
225
|
+
loading="lazy"
|
|
226
|
+
decoding="async"
|
|
227
|
+
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
|
228
|
+
{...a11y}
|
|
229
|
+
/>
|
|
230
|
+
<span
|
|
231
|
+
hidden
|
|
232
|
+
data-grove-icon-fallback-initials
|
|
233
|
+
class:list={[
|
|
234
|
+
"inline-flex shrink-0 select-none items-center justify-center rounded-sm bg-ink-100 align-middle font-semibold uppercase leading-none text-ink-700 dark:bg-ink-800 dark:text-ink-200",
|
|
235
|
+
className,
|
|
236
|
+
]}
|
|
237
|
+
style={`width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`}
|
|
238
|
+
{...a11y}
|
|
239
|
+
>
|
|
240
|
+
{init || "·"}
|
|
241
|
+
</span>
|
|
242
|
+
</>
|
|
171
243
|
)
|
|
172
244
|
}
|
|
173
245
|
|
|
174
|
-
{
|
|
246
|
+
{effectiveMode === "auto" && (
|
|
175
247
|
<script is:inline>
|
|
176
248
|
// Theme-aware icon swap. Runs after THEME_INIT (BaseLayout) so
|
|
177
249
|
// `data-resolved-theme` is already populated. Idempotent — the
|
|
@@ -29,6 +29,7 @@ interface Facets {
|
|
|
29
29
|
stacks?: FacetValue[];
|
|
30
30
|
platforms?: FacetValue[];
|
|
31
31
|
categories?: FacetValue[];
|
|
32
|
+
tags?: FacetValue[];
|
|
32
33
|
labels?: FacetValue[];
|
|
33
34
|
licenses?: FacetValue[];
|
|
34
35
|
}
|
|
@@ -39,7 +40,7 @@ interface Props {
|
|
|
39
40
|
pathPrefix?: string;
|
|
40
41
|
class?: string;
|
|
41
42
|
/** Hide specific facets. Default: all shown. */
|
|
42
|
-
hide?: Array<"stack" | "platform" | "category" | "license" | "sort">;
|
|
43
|
+
hide?: Array<"stack" | "platform" | "category" | "tag" | "license" | "sort">;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const {
|
|
@@ -52,7 +53,7 @@ const {
|
|
|
52
53
|
|
|
53
54
|
interface FilterGroup {
|
|
54
55
|
label: string;
|
|
55
|
-
filterKey: "stacks" | "platforms" | "categories" | "licenses";
|
|
56
|
+
filterKey: "stacks" | "platforms" | "categories" | "tags" | "licenses";
|
|
56
57
|
options: FacetValue[];
|
|
57
58
|
initial: string[];
|
|
58
59
|
single?: boolean;
|
|
@@ -68,6 +69,7 @@ const baseGroups: FilterGroup[] = [
|
|
|
68
69
|
{ label: "Stack", filterKey: "stacks", options: facets.stacks ?? [], initial: initial.stacks ?? [] },
|
|
69
70
|
{ label: "Platform", filterKey: "platforms", options: facets.platforms ?? [], initial: initial.platforms ?? [] },
|
|
70
71
|
{ label: "Category", filterKey: "categories", options: facets.categories ?? [], initial: initial.categories ?? [] },
|
|
72
|
+
{ label: "Tag", filterKey: "tags", options: facets.tags ?? [], initial: initial.tags ?? [] },
|
|
71
73
|
{ label: "License", filterKey: "licenses", options: facets.licenses ?? [], initial: initial.licenses ?? [], single: true },
|
|
72
74
|
];
|
|
73
75
|
|
|
@@ -86,7 +88,7 @@ function triggerLabel(group: FilterGroup): string {
|
|
|
86
88
|
|
|
87
89
|
const groups: RenderedFilterGroup[] = baseGroups
|
|
88
90
|
.filter((group) => {
|
|
89
|
-
const singular = group.filterKey.slice(0, -1) as "stack" | "platform" | "category" | "license";
|
|
91
|
+
const singular = group.filterKey.slice(0, -1) as "stack" | "platform" | "category" | "tag" | "license";
|
|
90
92
|
return !hide.includes(singular) && group.options.length > 0;
|
|
91
93
|
})
|
|
92
94
|
.map((group) => ({
|
|
@@ -97,8 +99,7 @@ const groups: RenderedFilterGroup[] = baseGroups
|
|
|
97
99
|
}));
|
|
98
100
|
---
|
|
99
101
|
|
|
100
|
-
<section class={className}>
|
|
101
|
-
<div class="flex flex-wrap items-center gap-2" role="toolbar" aria-label="Filter items">
|
|
102
|
+
<section class:list={["flex flex-wrap items-center gap-2", className]} role="toolbar" aria-label="Filter items">
|
|
102
103
|
{groups.map((group) => <FilterGroupMenu {...group} />)}
|
|
103
104
|
|
|
104
105
|
{!hide.includes("sort") && (
|
|
@@ -112,13 +113,12 @@ const groups: RenderedFilterGroup[] = baseGroups
|
|
|
112
113
|
</svg>
|
|
113
114
|
</label>
|
|
114
115
|
)}
|
|
115
|
-
</div>
|
|
116
116
|
</section>
|
|
117
117
|
|
|
118
118
|
<script is:inline define:vars={{ pathPrefix }}>
|
|
119
119
|
(() => {
|
|
120
120
|
const groups = document.querySelectorAll(".grove-filter");
|
|
121
|
-
const keys = { stacks: "stack", platforms: "platform", categories: "category", licenses: "license" };
|
|
121
|
+
const keys = { stacks: "stack", platforms: "platform", categories: "category", tags: "tag", licenses: "license" };
|
|
122
122
|
function closeAll(except) {
|
|
123
123
|
groups.forEach((group) => {
|
|
124
124
|
if (group === except) return;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SmartLensTabs — curated single-select lens tabs.
|
|
4
4
|
*
|
|
5
|
-
* Renders
|
|
6
|
-
*
|
|
5
|
+
* Renders configured curated lenses as a horizontal tab bar. Sort choices,
|
|
6
|
+
* including Recently added, belong in the separate sort control.
|
|
7
7
|
* Clicking a tab navigates to the same list page with the `lens=`
|
|
8
8
|
* query param updated. Other filter params (q, stack, platform,
|
|
9
9
|
* category, sort) are preserved.
|
|
@@ -55,12 +55,13 @@ const lenses =
|
|
|
55
55
|
<a
|
|
56
56
|
href={hrefForLens(lens.id, currentParams, pathPrefix)}
|
|
57
57
|
class:list={[
|
|
58
|
-
"inline-flex h-
|
|
58
|
+
"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",
|
|
59
59
|
active
|
|
60
|
-
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
60
|
+
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
|
|
61
61
|
: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
62
62
|
]}
|
|
63
63
|
aria-current={active ? "page" : undefined}
|
|
64
|
+
data-lens-id={lens.id}
|
|
64
65
|
data-event="lens_tab_click"
|
|
65
66
|
data-event-source="smart_lens_tabs"
|
|
66
67
|
>
|
|
@@ -94,7 +94,7 @@ function statusLabel(s: StackEntry["status"]): string | null {
|
|
|
94
94
|
data-event-source="stack_grid"
|
|
95
95
|
>
|
|
96
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.
|
|
97
|
+
<Icon name={iconAliases?.[s.slug] ?? s.slug} category="stack" size={18} />
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|