@grove-dev/astro 0.3.1 → 0.3.4
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 +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 +23 -11
- 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 +35 -7
- package/src/components/IndexRow.astro +2 -2
- package/src/components/ItemCard.astro +2 -7
- package/src/components/RefinePanel.astro +7 -7
- package/src/components/SmartLensTabs.astro +9 -5
- 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 +30 -8
- 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} +12 -8
- package/src/server/index.ts +2 -0
- package/src/server/models.ts +436 -0
- package/dist/template-routes.test.d.ts +0 -2
- package/dist/template-routes.test.d.ts.map +0 -1
- package/dist/template-routes.test.js +0 -63
- package/dist/template-routes.test.js.map +0 -1
- package/dist/theme.test.d.ts +0 -2
- package/dist/theme.test.d.ts.map +0 -1
- package/dist/theme.test.js +0 -60
- package/dist/theme.test.js.map +0 -1
- package/src/template-routes.test.ts +0 -103
- package/src/theme.test.ts +0 -76
- 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 -53
- package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -46
- package/templates/default/.github/workflows/daily-refresh.yml +0 -51
- package/templates/default/.github/workflows/sync-contributors.yml +0 -51
- package/templates/default/.github/workflows/sync-github-metadata.yml +0 -56
- package/templates/default/.github/workflows/validate-data.yml +0 -33
- 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 -43
- 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 -83
- package/templates/default/public/llms.txt +0 -11
- 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/src/pages/404.astro +0 -64
- package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -691
- package/templates/default/src/pages/[slug]/index.astro +0 -482
- package/templates/default/src/pages/about.astro +0 -189
- package/templates/default/src/pages/apps/[recordSlug].astro +0 -34
- package/templates/default/src/pages/contributors.astro +0 -175
- package/templates/default/src/pages/index.astro +0 -233
- 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/tsconfig.json +0 -9
- 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
|
+
* Optional complete asset registry supplied by a consumer. When omitted,
|
|
67
|
+
* Grove attempts the configured asset path and falls back on image error.
|
|
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,12 @@ function resolveName(raw: string): string {
|
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
const resolved = resolveName(name);
|
|
127
|
+
const availableIcons = available?.map(slugify);
|
|
128
|
+
const hasAsset =
|
|
129
|
+
category === "brand" ||
|
|
130
|
+
availableIcons === undefined ||
|
|
131
|
+
availableIcons.includes(resolved);
|
|
132
|
+
const effectiveMode: IconMode = resolved === "apple" ? "auto" : mode;
|
|
107
133
|
const platformStackIcons = new Set(["docker"]);
|
|
108
134
|
const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
|
|
109
135
|
const folder =
|
|
@@ -123,7 +149,9 @@ const init = (fallbackInitials ?? name ?? "")
|
|
|
123
149
|
.slice(0, 2)
|
|
124
150
|
.toUpperCase();
|
|
125
151
|
const showFallback =
|
|
126
|
-
!resolved ||
|
|
152
|
+
!resolved ||
|
|
153
|
+
!hasAsset ||
|
|
154
|
+
(category === "platform" && textOnlyPlatforms.has(resolved));
|
|
127
155
|
|
|
128
156
|
// Build aria-label.
|
|
129
157
|
const ariaLabel = title ?? name;
|
|
@@ -144,14 +172,14 @@ const a11y: Record<string, string> = title
|
|
|
144
172
|
>
|
|
145
173
|
{init || "·"}
|
|
146
174
|
</span>
|
|
147
|
-
) :
|
|
175
|
+
) : effectiveMode === "auto" ? (
|
|
148
176
|
<>
|
|
149
177
|
<img
|
|
150
|
-
src={
|
|
178
|
+
src={`${baseUrl}-light.svg`}
|
|
151
179
|
data-grove-icon
|
|
152
180
|
data-grove-icon-light={`${baseUrl}-light.svg`}
|
|
153
181
|
data-grove-icon-dark={`${baseUrl}-dark.svg`}
|
|
154
|
-
data-grove-icon-fallback={
|
|
182
|
+
data-grove-icon-fallback={`${baseUrl}-light.svg`}
|
|
155
183
|
width={size}
|
|
156
184
|
height={size}
|
|
157
185
|
alt=""
|
|
@@ -201,7 +229,7 @@ const a11y: Record<string, string> = title
|
|
|
201
229
|
)
|
|
202
230
|
}
|
|
203
231
|
|
|
204
|
-
{
|
|
232
|
+
{effectiveMode === "auto" && (
|
|
205
233
|
<script is:inline>
|
|
206
234
|
// Theme-aware icon swap. Runs after THEME_INIT (BaseLayout) so
|
|
207
235
|
// `data-resolved-theme` is already populated. Idempotent — the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { IndexRecord } from "@grove-dev/core";
|
|
3
3
|
import Icon from "./Icon.astro";
|
|
4
|
-
import { formatRelative, formatStars, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, statusDisplay } from "@grove-dev/astro";
|
|
4
|
+
import { formatRelative, formatStars, getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds, statusDisplay } from "@grove-dev/astro";
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
record: IndexRecord;
|
|
@@ -23,7 +23,7 @@ const repoHref = project?.repoUrl ?? item.links.github;
|
|
|
23
23
|
const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
|
|
24
24
|
const avatar = project ? project.logoUrl ?? getOwnerAvatarUrl(owner, 80) : undefined;
|
|
25
25
|
const initials = name.split(/\s+/).map((word) => word[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();
|
|
26
|
-
const stacks = project
|
|
26
|
+
const stacks = projectStackIds(project);
|
|
27
27
|
const platforms = project?.platforms ?? [];
|
|
28
28
|
const stars = project?.github?.stars;
|
|
29
29
|
const updatedAt = project?.github?.pushedAt ?? (item.kind === "resource" ? item.publishedAt : null);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
27
27
|
import Icon from "./Icon.astro";
|
|
28
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
|
|
28
|
+
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, projectStackIds } from "@grove-dev/astro";
|
|
29
29
|
import { formatStars, formatRelative } from "@grove-dev/astro";
|
|
30
30
|
import { statusDisplay } from "@grove-dev/astro";
|
|
31
31
|
|
|
@@ -126,12 +126,7 @@ const platforms = isProject ? (item as { platforms?: string[] }).platforms ?? []
|
|
|
126
126
|
const visiblePlatforms = platforms.slice(0, 4);
|
|
127
127
|
const platformOverflow = platforms.length - visiblePlatforms.length;
|
|
128
128
|
|
|
129
|
-
const allStacks = isProject
|
|
130
|
-
? [
|
|
131
|
-
(item as { stack?: string }).stack,
|
|
132
|
-
...((item as { stacks?: string[] }).stacks ?? []),
|
|
133
|
-
].filter((s): s is string => Boolean(s))
|
|
134
|
-
: [];
|
|
129
|
+
const allStacks = isProject ? projectStackIds(item as ProjectRecord) : [];
|
|
135
130
|
const visibleStacks = allStacks.slice(0, 3);
|
|
136
131
|
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
137
132
|
|
|
@@ -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.
|
|
@@ -24,6 +24,8 @@ interface Props {
|
|
|
24
24
|
class?: string;
|
|
25
25
|
/** Override the visible lens list (defaults to LENSES). */
|
|
26
26
|
lensIds?: string[];
|
|
27
|
+
/** Consumer noun for the unfiltered lens, e.g. "All apps". */
|
|
28
|
+
allLabel?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const {
|
|
@@ -31,6 +33,7 @@ const {
|
|
|
31
33
|
pathPrefix = "/",
|
|
32
34
|
class: className = "",
|
|
33
35
|
lensIds,
|
|
36
|
+
allLabel,
|
|
34
37
|
} = Astro.props;
|
|
35
38
|
|
|
36
39
|
// If `lensIds` is provided, filter LENSES to those ids in that order.
|
|
@@ -55,16 +58,17 @@ const lenses =
|
|
|
55
58
|
<a
|
|
56
59
|
href={hrefForLens(lens.id, currentParams, pathPrefix)}
|
|
57
60
|
class:list={[
|
|
58
|
-
"inline-flex h-
|
|
61
|
+
"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
62
|
active
|
|
60
|
-
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
63
|
+
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
|
|
61
64
|
: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
62
65
|
]}
|
|
63
66
|
aria-current={active ? "page" : undefined}
|
|
67
|
+
data-lens-id={lens.id}
|
|
64
68
|
data-event="lens_tab_click"
|
|
65
69
|
data-event-source="smart_lens_tabs"
|
|
66
70
|
>
|
|
67
|
-
{lensDisplay(lens.id)}
|
|
71
|
+
{lens.id === "all" && allLabel ? allLabel : lensDisplay(lens.id)}
|
|
68
72
|
</a>
|
|
69
73
|
);
|
|
70
74
|
})}
|
|
@@ -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">
|