@grove-dev/astro 0.1.5 → 0.2.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/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppSection.astro +83 -0
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +289 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/.astro/content.d.ts +155 -0
- package/templates/default/.astro/settings.json +5 -0
- package/templates/default/.astro/types.d.ts +2 -0
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +1 -0
- package/templates/default/dist/about/index.html +165 -0
- package/templates/default/dist/icons/platforms/android.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-dark.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-light.svg +1 -0
- package/templates/default/dist/icons/platforms/chrome.svg +1 -0
- package/templates/default/dist/icons/platforms/chromeos.svg +5 -0
- package/templates/default/dist/icons/platforms/desktop.svg +4 -0
- package/templates/default/dist/icons/platforms/embedded.svg +5 -0
- package/templates/default/dist/icons/platforms/ios.svg +1 -0
- package/templates/default/dist/icons/platforms/linux.svg +1 -0
- package/templates/default/dist/icons/platforms/macos.svg +1 -0
- package/templates/default/dist/icons/platforms/ubuntu.svg +1 -0
- package/templates/default/dist/icons/platforms/web.svg +5 -0
- package/templates/default/dist/icons/platforms/windows.svg +6 -0
- package/templates/default/dist/icons/stacks/android.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-dark.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-light.svg +1 -0
- package/templates/default/dist/icons/stacks/capacitor.svg +14 -0
- package/templates/default/dist/icons/stacks/dart.svg +1 -0
- package/templates/default/dist/icons/stacks/firebase.svg +1 -0
- package/templates/default/dist/icons/stacks/flutter.svg +1 -0
- package/templates/default/dist/icons/stacks/graphql.svg +1 -0
- package/templates/default/dist/icons/stacks/ionic.svg +4 -0
- package/templates/default/dist/icons/stacks/java.svg +1 -0
- package/templates/default/dist/icons/stacks/javascript.svg +1 -0
- package/templates/default/dist/icons/stacks/kotlin.svg +1 -0
- package/templates/default/dist/icons/stacks/mongodb.svg +1 -0
- package/templates/default/dist/icons/stacks/nodejs.svg +1 -0
- package/templates/default/dist/icons/stacks/python.svg +1 -0
- package/templates/default/dist/icons/stacks/react-native.svg +1 -0
- package/templates/default/dist/icons/stacks/react.svg +1 -0
- package/templates/default/dist/icons/stacks/rust.svg +1 -0
- package/templates/default/dist/icons/stacks/solidity.svg +1 -0
- package/templates/default/dist/icons/stacks/swift.svg +1 -0
- package/templates/default/dist/icons/stacks/tensorflow.svg +1 -0
- package/templates/default/dist/icons/stacks/typescript.svg +1 -0
- package/templates/default/dist/index.html +55 -0
- package/templates/default/dist/llms-full.txt +14 -0
- package/templates/default/dist/llms.txt +11 -0
- package/templates/default/dist/og-image.svg +48 -0
- package/templates/default/dist/projects/index.html +61 -0
- package/templates/default/dist/robots.txt +53 -0
- package/templates/default/dist/sitemap.xml +21 -0
- package/templates/default/dist/submit/index.html +69 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/node_modules/.bin/astro +21 -0
- package/templates/default/node_modules/.bin/astro-check +21 -0
- package/templates/default/node_modules/.bin/grove +21 -0
- package/templates/default/node_modules/.bin/jiti +21 -0
- package/templates/default/node_modules/.bin/prettier +21 -0
- package/templates/default/node_modules/.bin/rollup +21 -0
- package/templates/default/node_modules/.bin/tsc +21 -0
- package/templates/default/node_modules/.bin/tsserver +21 -0
- package/templates/default/node_modules/.bin/tsx +21 -0
- package/templates/default/node_modules/.bin/vite +21 -0
- package/templates/default/node_modules/.bin/yaml +21 -0
- package/templates/default/node_modules/.vite/deps/_metadata.json +79 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +433 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +9 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +241 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +1592 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +186 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +48 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +6779 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +33 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +88 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +72 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +3757 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +54 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/package.json +3 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +213 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +525 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +1020 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +147 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +7 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +161 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +258 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- package/templates/default/src/pages/contributors.astro +0 -142
|
@@ -1,669 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { siteConfig } from "../../data/config";
|
|
3
|
-
import BaseLayout from "../../components/layout/BaseLayout.astro";
|
|
4
|
-
import Container from "../../components/layout/Container.astro";
|
|
5
|
-
import SmartLensTabs from "../../components/SmartLensTabs.astro";
|
|
6
|
-
import RefinePanel from "../../components/RefinePanel.astro";
|
|
7
|
-
import AppsIndexRow from "../../components/AppsIndexRow.astro";
|
|
8
|
-
import AppsPagination from "../../components/AppsPagination.astro";
|
|
9
|
-
import ExploreByCategory from "../../components/ExploreByCategory.astro";
|
|
10
|
-
import ExploreByStack from "../../components/ExploreByStack.astro";
|
|
11
|
-
|
|
12
|
-
import { apps } from "../../data/apps";
|
|
13
|
-
import {
|
|
14
|
-
buildFacets,
|
|
15
|
-
filtersFromSearchParams,
|
|
16
|
-
searchParamsFromFilters,
|
|
17
|
-
filterApps,
|
|
18
|
-
activeFilterChips,
|
|
19
|
-
applySort,
|
|
20
|
-
paginate,
|
|
21
|
-
totalPages,
|
|
22
|
-
PAGE_SIZE,
|
|
23
|
-
effectiveSort,
|
|
24
|
-
effectivePage,
|
|
25
|
-
effectiveDensity,
|
|
26
|
-
SORT_OPTIONS,
|
|
27
|
-
type AppsFilters,
|
|
28
|
-
} from "../../lib/apps-search";
|
|
29
|
-
import { lensFromSearchParams } from "../../lib/lenses";
|
|
30
|
-
import { categories } from "../../data/categories";
|
|
31
|
-
import { stacks } from "../../data/stacks";
|
|
32
|
-
import { stats } from "../../data/stats";
|
|
33
|
-
|
|
34
|
-
// Parse URL → filters, then derive the effective (post-default) values.
|
|
35
|
-
const initialFilters = filtersFromSearchParams(Astro.url.searchParams);
|
|
36
|
-
const initialLens = lensFromSearchParams(Astro.url.searchParams);
|
|
37
|
-
const facets = buildFacets(apps);
|
|
38
|
-
const sort = effectiveSort(initialFilters);
|
|
39
|
-
const page = effectivePage(initialFilters);
|
|
40
|
-
const density = effectiveDensity(initialFilters);
|
|
41
|
-
const initialChips = activeFilterChips(initialFilters);
|
|
42
|
-
|
|
43
|
-
// Apply filter → sort → paginate server-side. URL is the source of
|
|
44
|
-
// truth; this page is fully SSR. No client-side render loop.
|
|
45
|
-
const filtered = filterApps(apps, initialFilters);
|
|
46
|
-
const sorted = applySort(filtered, sort);
|
|
47
|
-
const totalFiltered = sorted.length;
|
|
48
|
-
const totalFilteredPages = totalPages(totalFiltered);
|
|
49
|
-
const safePage = Math.min(page, totalFilteredPages);
|
|
50
|
-
const pageApps = paginate(sorted, safePage);
|
|
51
|
-
const initialChipsTotal = initialChips.length;
|
|
52
|
-
|
|
53
|
-
const githubUrl = stats.originalRepo;
|
|
54
|
-
|
|
55
|
-
// Build a "next page" / "prev page" href that preserves every other
|
|
56
|
-
// filter param. Reused by the pagination component.
|
|
57
|
-
function hrefForPage(target: number): string {
|
|
58
|
-
const next: AppsFilters = { ...initialFilters, page: target };
|
|
59
|
-
const sp = searchParamsFromFilters(next);
|
|
60
|
-
const qs = sp.toString();
|
|
61
|
-
return qs ? `/apps?${qs}` : "/apps";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function hrefForDensity(value: "comfortable" | "compact"): string {
|
|
65
|
-
const next: AppsFilters = { ...initialFilters, density: value, page: 1 };
|
|
66
|
-
const sp = searchParamsFromFilters(next);
|
|
67
|
-
const qs = sp.toString();
|
|
68
|
-
return qs ? `/apps?${qs}` : "/apps";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function hrefForClearAll(): string {
|
|
72
|
-
return "/apps";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Human-readable summary line under the search.
|
|
76
|
-
const sortLabel = SORT_OPTIONS.find((o) => o.value === sort)?.label ?? "Recently updated";
|
|
77
|
-
const filterSummary: string[] = [];
|
|
78
|
-
if (initialFilters.q) filterSummary.push(`matching "${initialFilters.q}"`);
|
|
79
|
-
if (initialFilters.stacks?.length) filterSummary.push(`stack: ${initialFilters.stacks.join(", ")}`);
|
|
80
|
-
if (initialFilters.platforms?.length)
|
|
81
|
-
filterSummary.push(`platform: ${initialFilters.platforms.join(", ")}`);
|
|
82
|
-
if (initialFilters.categories?.length)
|
|
83
|
-
filterSummary.push(`category: ${initialFilters.categories.join(", ")}`);
|
|
84
|
-
if (initialFilters.licenses?.length)
|
|
85
|
-
filterSummary.push(`license: ${initialFilters.licenses.join(", ")}`);
|
|
86
|
-
if (initialFilters.labels?.length) filterSummary.push(`label: ${initialFilters.labels.join(", ")}`);
|
|
87
|
-
if (initialLens) filterSummary.push(`lens: ${initialLens}`);
|
|
88
|
-
|
|
89
|
-
const title = "Browse open-source apps — ${siteConfig.name}";
|
|
90
|
-
const description =
|
|
91
|
-
"Search and filter real open-source applications by platform, stack, category, license, and activity.";
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
<BaseLayout title={title} description={description}>
|
|
95
|
-
{/* ── Top: title + search + sticky toolbar ── */}
|
|
96
|
-
<section
|
|
97
|
-
class="border-b border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950"
|
|
98
|
-
>
|
|
99
|
-
<Container>
|
|
100
|
-
<div class="py-8 sm:py-10">
|
|
101
|
-
<span
|
|
102
|
-
class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
103
|
-
>Apps</span
|
|
104
|
-
>
|
|
105
|
-
<h1
|
|
106
|
-
class="mt-1 m-0 text-[1.75rem] sm:text-[2rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100"
|
|
107
|
-
>
|
|
108
|
-
Browse open-source apps
|
|
109
|
-
</h1>
|
|
110
|
-
<p
|
|
111
|
-
class="mt-2 m-0 max-w-[64ch] text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400"
|
|
112
|
-
>
|
|
113
|
-
Search and filter real open-source applications by platform,
|
|
114
|
-
stack, category, license, and activity. Every entry is a real
|
|
115
|
-
codebase with hand-curated context.
|
|
116
|
-
</p>
|
|
117
|
-
|
|
118
|
-
{/* Sticky toolbar. Holds: search, results summary, sort,
|
|
119
|
-
density toggle. Once it sticks on scroll, the chips
|
|
120
|
-
layer + refine button follow along below. */}
|
|
121
|
-
<div
|
|
122
|
-
id="apps-toolbar"
|
|
123
|
-
class="sticky top-[57px] z-30 mt-6 -mx-5 border-y border-ink-200 bg-white/95 px-5 backdrop-blur supports-[backdrop-filter]:bg-white/80 dark:border-ink-800 dark:bg-ink-950/95 dark:supports-[backdrop-filter]:bg-ink-950/80 sm:-mx-7 sm:px-7"
|
|
124
|
-
>
|
|
125
|
-
<form
|
|
126
|
-
id="search-form"
|
|
127
|
-
class="flex flex-col gap-3 py-3 sm:flex-row sm:items-center"
|
|
128
|
-
role="search"
|
|
129
|
-
>
|
|
130
|
-
<label class="relative flex-1">
|
|
131
|
-
<span class="sr-only">Search apps by name, description, owner, category</span>
|
|
132
|
-
<svg
|
|
133
|
-
viewBox="0 0 16 16"
|
|
134
|
-
width="16"
|
|
135
|
-
height="16"
|
|
136
|
-
aria-hidden="true"
|
|
137
|
-
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
|
|
138
|
-
fill="currentColor"
|
|
139
|
-
>
|
|
140
|
-
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z" />
|
|
141
|
-
</svg>
|
|
142
|
-
<input
|
|
143
|
-
id="search-input"
|
|
144
|
-
type="search"
|
|
145
|
-
name="q"
|
|
146
|
-
value={initialFilters.q ?? ""}
|
|
147
|
-
placeholder="Search by app name, category, stack, platform, or license..."
|
|
148
|
-
autocomplete="off"
|
|
149
|
-
class="w-full rounded-md border border-ink-200 bg-white py-2 pl-9 pr-3 text-[0.875rem] 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"
|
|
150
|
-
/>
|
|
151
|
-
</label>
|
|
152
|
-
|
|
153
|
-
{/* Sort dropdown */}
|
|
154
|
-
<label class="relative">
|
|
155
|
-
<span class="sr-only">Sort apps</span>
|
|
156
|
-
<select
|
|
157
|
-
id="sort-select"
|
|
158
|
-
class="h-9 cursor-pointer appearance-none rounded-md border border-ink-200 bg-white py-1.5 pl-3 pr-8 text-[0.8125rem] font-medium text-ink-700 transition-colors hover:border-ink-300 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
|
|
159
|
-
>
|
|
160
|
-
{SORT_OPTIONS.map((o) => (
|
|
161
|
-
<option value={o.value} selected={o.value === sort}>{o.label}</option>
|
|
162
|
-
))}
|
|
163
|
-
</select>
|
|
164
|
-
<svg
|
|
165
|
-
viewBox="0 0 16 16"
|
|
166
|
-
width="11"
|
|
167
|
-
height="11"
|
|
168
|
-
aria-hidden="true"
|
|
169
|
-
class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
170
|
-
fill="currentColor"
|
|
171
|
-
>
|
|
172
|
-
<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"/>
|
|
173
|
-
</svg>
|
|
174
|
-
</label>
|
|
175
|
-
|
|
176
|
-
{/* Density toggle: comfortable / compact */}
|
|
177
|
-
<div
|
|
178
|
-
class="inline-flex h-9 items-center rounded-md border border-ink-200 bg-white p-0.5 dark:border-ink-800 dark:bg-ink-950"
|
|
179
|
-
role="group"
|
|
180
|
-
aria-label="List density"
|
|
181
|
-
>
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
data-density-toggle="comfortable"
|
|
185
|
-
data-href={hrefForDensity("comfortable")}
|
|
186
|
-
class={`inline-flex h-7 items-center rounded-[5px] px-2 text-[0.75rem] font-medium transition-colors ${
|
|
187
|
-
density === "comfortable"
|
|
188
|
-
? "bg-ink-900 text-white dark:bg-ink-100 dark:text-ink-900"
|
|
189
|
-
: "text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
190
|
-
}`}
|
|
191
|
-
aria-pressed={density === "comfortable" ? "true" : "false"}
|
|
192
|
-
>
|
|
193
|
-
Comfortable
|
|
194
|
-
</button>
|
|
195
|
-
<button
|
|
196
|
-
type="button"
|
|
197
|
-
data-density-toggle="compact"
|
|
198
|
-
data-href={hrefForDensity("compact")}
|
|
199
|
-
class={`inline-flex h-7 items-center rounded-[5px] px-2 text-[0.75rem] font-medium transition-colors ${
|
|
200
|
-
density === "compact"
|
|
201
|
-
? "bg-ink-900 text-white dark:bg-ink-100 dark:text-ink-900"
|
|
202
|
-
: "text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
203
|
-
}`}
|
|
204
|
-
aria-pressed={density === "compact" ? "true" : "false"}
|
|
205
|
-
>
|
|
206
|
-
Compact
|
|
207
|
-
</button>
|
|
208
|
-
</div>
|
|
209
|
-
</form>
|
|
210
|
-
|
|
211
|
-
{/* Result count summary + active filter chips row. */}
|
|
212
|
-
<div
|
|
213
|
-
id="result-summary"
|
|
214
|
-
class="flex flex-col gap-2 border-t border-ink-100 py-2.5 text-2xs dark:border-ink-900 sm:flex-row sm:items-center sm:justify-between"
|
|
215
|
-
>
|
|
216
|
-
<p class="m-0 text-ink-500 dark:text-ink-400">
|
|
217
|
-
<span
|
|
218
|
-
id="apps-results-count"
|
|
219
|
-
class="font-mono text-ink-900 dark:text-ink-100"
|
|
220
|
-
data-total={totalFiltered}
|
|
221
|
-
data-page-count={pageApps.length}
|
|
222
|
-
data-page-size={PAGE_SIZE}
|
|
223
|
-
data-page={safePage}
|
|
224
|
-
data-pages={totalFilteredPages}
|
|
225
|
-
>
|
|
226
|
-
{totalFiltered} {totalFiltered === 1 ? "app" : "apps"} found
|
|
227
|
-
</span>
|
|
228
|
-
{filterSummary.length > 0 && (
|
|
229
|
-
<span id="apps-filters-summary" class="text-ink-400 dark:text-ink-500">
|
|
230
|
-
{" · "}filtered by {filterSummary.join(" · ")}
|
|
231
|
-
</span>
|
|
232
|
-
)}
|
|
233
|
-
<span class="text-ink-400 dark:text-ink-500">
|
|
234
|
-
{" · "}sorted by {sortLabel}
|
|
235
|
-
</span>
|
|
236
|
-
{totalFilteredPages > 1 && (
|
|
237
|
-
<span class="text-ink-400 dark:text-ink-500">
|
|
238
|
-
{" · "}page {safePage} of {totalFilteredPages}
|
|
239
|
-
</span>
|
|
240
|
-
)}
|
|
241
|
-
</p>
|
|
242
|
-
|
|
243
|
-
{initialChipsTotal > 0 && (
|
|
244
|
-
<div
|
|
245
|
-
id="apps-active-chips"
|
|
246
|
-
class="flex flex-wrap items-center gap-1.5"
|
|
247
|
-
aria-label="Active filters"
|
|
248
|
-
>
|
|
249
|
-
{initialChips.map((chip) => (
|
|
250
|
-
<button
|
|
251
|
-
type="button"
|
|
252
|
-
class="apps-active-chip group inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
|
|
253
|
-
data-remove-key={chip.key}
|
|
254
|
-
data-remove-value={chip.value}
|
|
255
|
-
aria-label={`Remove filter ${chip.label}`}
|
|
256
|
-
>
|
|
257
|
-
<span>{chip.label}</span>
|
|
258
|
-
<span aria-hidden="true" class="text-ink-400 group-hover:text-ink-700 dark:text-ink-500 dark:group-hover:text-ink-200">×</span>
|
|
259
|
-
</button>
|
|
260
|
-
))}
|
|
261
|
-
<a
|
|
262
|
-
id="apps-clear-all"
|
|
263
|
-
href={hrefForClearAll()}
|
|
264
|
-
class="text-2xs text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
265
|
-
>
|
|
266
|
-
Clear all
|
|
267
|
-
</a>
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
</Container>
|
|
274
|
-
</section>
|
|
275
|
-
|
|
276
|
-
{/* ── Lenses + Filter bar — single toolbar that combines
|
|
277
|
-
"what lens am I in" (curated views) with "what fields
|
|
278
|
-
are filtered" (drop-down filters). Stays compact. ── */}
|
|
279
|
-
<section class="border-b border-ink-200 dark:border-ink-800">
|
|
280
|
-
<Container>
|
|
281
|
-
<div class="flex flex-col gap-3 py-4">
|
|
282
|
-
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
283
|
-
<span
|
|
284
|
-
class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
285
|
-
>Lenses</span
|
|
286
|
-
>
|
|
287
|
-
<SmartLensTabs currentParams={Astro.url.searchParams} />
|
|
288
|
-
</div>
|
|
289
|
-
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
290
|
-
<span
|
|
291
|
-
class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
292
|
-
>Filters</span
|
|
293
|
-
>
|
|
294
|
-
<RefinePanel
|
|
295
|
-
facets={facets}
|
|
296
|
-
initial={initialFilters}
|
|
297
|
-
/>
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
</Container>
|
|
301
|
-
</section>
|
|
302
|
-
|
|
303
|
-
{/* ── Results ── */}
|
|
304
|
-
<section class="border-b border-ink-200 dark:border-ink-800">
|
|
305
|
-
<Container>
|
|
306
|
-
<div class="py-8 sm:py-10">
|
|
307
|
-
{pageApps.length === 0 ? (
|
|
308
|
-
<div
|
|
309
|
-
id="empty-state"
|
|
310
|
-
class="flex flex-col items-center gap-2 py-16 text-center"
|
|
311
|
-
>
|
|
312
|
-
<p class="m-0 text-[0.9375rem] font-medium text-ink-700 dark:text-ink-200">
|
|
313
|
-
No apps match these filters.
|
|
314
|
-
</p>
|
|
315
|
-
<p class="m-0 max-w-[40ch] text-2xs text-ink-500 dark:text-ink-400">
|
|
316
|
-
Try removing a filter, clearing the search, or picking a
|
|
317
|
-
different lens.
|
|
318
|
-
</p>
|
|
319
|
-
<a href={hrefForClearAll()} class="mt-3 btn-outline">Clear all filters</a>
|
|
320
|
-
</div>
|
|
321
|
-
) : (
|
|
322
|
-
<div
|
|
323
|
-
id="apps-results-list"
|
|
324
|
-
data-density={density}
|
|
325
|
-
class={`flex flex-col gap-3 ${density === "compact" ? "apps-density-compact" : ""}`}
|
|
326
|
-
>
|
|
327
|
-
{pageApps.map((app) => <AppsIndexRow app={app} />)}
|
|
328
|
-
</div>
|
|
329
|
-
)}
|
|
330
|
-
|
|
331
|
-
<AppsPagination
|
|
332
|
-
current={safePage}
|
|
333
|
-
total={totalFilteredPages}
|
|
334
|
-
hrefFor={hrefForPage}
|
|
335
|
-
/>
|
|
336
|
-
</div>
|
|
337
|
-
</Container>
|
|
338
|
-
</section>
|
|
339
|
-
|
|
340
|
-
{/* ── Explore: navigation (not filters) ── */}
|
|
341
|
-
<ExploreByCategory categories={categories} />
|
|
342
|
-
<ExploreByStack stacks={stacks} />
|
|
343
|
-
|
|
344
|
-
{/* ── Submit CTA ── */}
|
|
345
|
-
<section class="border-t border-ink-200 dark:border-ink-800">
|
|
346
|
-
<Container>
|
|
347
|
-
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
|
348
|
-
<h2
|
|
349
|
-
class="m-0 text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
350
|
-
>
|
|
351
|
-
Know a real open-source app that belongs here?
|
|
352
|
-
</h2>
|
|
353
|
-
<p
|
|
354
|
-
class="m-0 max-w-[48ch] text-[0.875rem] text-ink-500 dark:text-ink-400"
|
|
355
|
-
>
|
|
356
|
-
Open a pull request with a YAML file. We review every
|
|
357
|
-
submission against the bar — no marketing, no tutorials.
|
|
358
|
-
</p>
|
|
359
|
-
<a
|
|
360
|
-
href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
|
|
361
|
-
target="_blank"
|
|
362
|
-
rel="noopener noreferrer"
|
|
363
|
-
class="btn-primary"
|
|
364
|
-
>Submit via GitHub</a
|
|
365
|
-
>
|
|
366
|
-
</div>
|
|
367
|
-
</Container>
|
|
368
|
-
</section>
|
|
369
|
-
{/* Initial state for the search form is server-rendered; this script
|
|
370
|
-
only handles debounced search updates + chip removal so the URL
|
|
371
|
-
stays canonical and SSR matches client expectations. */}
|
|
372
|
-
<script>
|
|
373
|
-
import {
|
|
374
|
-
filtersFromSearchParams,
|
|
375
|
-
searchParamsFromFilters,
|
|
376
|
-
type AppsFilters,
|
|
377
|
-
} from "../../lib/apps-search";
|
|
378
|
-
|
|
379
|
-
function readFilters(): AppsFilters {
|
|
380
|
-
return filtersFromSearchParams(new URLSearchParams(location.search));
|
|
381
|
-
}
|
|
382
|
-
function writeFilters(f: AppsFilters) {
|
|
383
|
-
const sp = searchParamsFromFilters(f);
|
|
384
|
-
const qs = sp.toString();
|
|
385
|
-
const url = qs ? `?${qs}` : location.pathname;
|
|
386
|
-
location.assign(url);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── Client-side row filter ─────────────────────────────────────
|
|
390
|
-
//
|
|
391
|
-
// The page is statically rendered (one HTML for every URL), so
|
|
392
|
-
// `?stack=flutter` etc. don't re-render the result list on the
|
|
393
|
-
// server. The RefinePanel calls `window.__applyFilters({...})` to
|
|
394
|
-
// push a new filter state; we hide non-matching rows, update the
|
|
395
|
-
// active-chip strip, the summary line, and the trigger labels
|
|
396
|
-
// — all without a full page reload.
|
|
397
|
-
//
|
|
398
|
-
// Each `.apps-row` already carries `data-stack`, `data-stacks`,
|
|
399
|
-
// `data-category`, `data-platforms`, `data-labels`, `data-status`,
|
|
400
|
-
// `data-license`, `data-stars`, `data-updated`, `data-name`,
|
|
401
|
-
// `data-q` (description, lowercased) for matching.
|
|
402
|
-
|
|
403
|
-
type RowEl = HTMLElement & {
|
|
404
|
-
dataset: {
|
|
405
|
-
stack?: string;
|
|
406
|
-
stacks?: string;
|
|
407
|
-
category?: string;
|
|
408
|
-
platforms?: string;
|
|
409
|
-
labels?: string;
|
|
410
|
-
status?: string;
|
|
411
|
-
license?: string;
|
|
412
|
-
stars?: string;
|
|
413
|
-
updated?: string;
|
|
414
|
-
name?: string;
|
|
415
|
-
q?: string;
|
|
416
|
-
};
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
const rows = Array.from(
|
|
420
|
-
document.querySelectorAll<RowEl>(".apps-row[data-slug]"),
|
|
421
|
-
);
|
|
422
|
-
const resultsList = document.getElementById("apps-results-list");
|
|
423
|
-
const emptyState = document.getElementById("apps-empty-state");
|
|
424
|
-
const resultsCount = document.getElementById("apps-results-count");
|
|
425
|
-
const filtersSummary = document.getElementById("apps-filters-summary");
|
|
426
|
-
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement | null;
|
|
427
|
-
const searchInput = document.getElementById("search-input") as HTMLInputElement | null;
|
|
428
|
-
const activeChipsEl = document.getElementById("apps-active-chips");
|
|
429
|
-
const clearAllBtn = document.getElementById("apps-clear-all") as HTMLAnchorElement | null;
|
|
430
|
-
const triggerLabels = new Map<string, HTMLElement>();
|
|
431
|
-
|
|
432
|
-
document.querySelectorAll<HTMLElement>(".apps-filter").forEach((g) => {
|
|
433
|
-
const key = g.dataset.filterGroupKey!;
|
|
434
|
-
const label = g.querySelector<HTMLElement>(".apps-filter-trigger > span:first-child");
|
|
435
|
-
if (label) triggerLabels.set(key, label);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
function pillFor(key: string, value: string): HTMLElement {
|
|
439
|
-
const el = document.createElement("button");
|
|
440
|
-
el.type = "button";
|
|
441
|
-
el.className =
|
|
442
|
-
"apps-active-chip inline-flex items-center gap-1 rounded-full border border-ink-200 bg-white px-2.5 py-1 text-2xs text-ink-700 transition-colors hover:border-ink-300 dark:border-ink-700 dark:bg-ink-900/40 dark:text-ink-200";
|
|
443
|
-
el.dataset.removeKey = key;
|
|
444
|
-
el.dataset.removeValue = value;
|
|
445
|
-
const v = document.createElement("span");
|
|
446
|
-
v.textContent = value;
|
|
447
|
-
el.appendChild(v);
|
|
448
|
-
const x = document.createElement("span");
|
|
449
|
-
x.setAttribute("aria-hidden", "true");
|
|
450
|
-
x.textContent = "×";
|
|
451
|
-
el.appendChild(x);
|
|
452
|
-
return el;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function renderChips(f: AppsFilters) {
|
|
456
|
-
if (!activeChipsEl) return;
|
|
457
|
-
activeChipsEl.innerHTML = "";
|
|
458
|
-
const chips: HTMLElement[] = [];
|
|
459
|
-
if (f.q) {
|
|
460
|
-
const e = pillFor("q", f.q);
|
|
461
|
-
const v = e.querySelector("span")!;
|
|
462
|
-
v.textContent = `“${f.q}”`;
|
|
463
|
-
chips.push(e);
|
|
464
|
-
}
|
|
465
|
-
for (const v of f.stacks ?? []) chips.push(pillFor("stacks", v));
|
|
466
|
-
for (const v of f.platforms ?? []) chips.push(pillFor("platforms", v));
|
|
467
|
-
for (const v of f.categories ?? []) chips.push(pillFor("categories", v));
|
|
468
|
-
for (const v of f.labels ?? []) chips.push(pillFor("labels", v));
|
|
469
|
-
for (const v of f.licenses ?? []) chips.push(pillFor("licenses", v));
|
|
470
|
-
for (const e of chips) activeChipsEl.appendChild(e);
|
|
471
|
-
if (clearAllBtn) {
|
|
472
|
-
if (chips.length > 0) {
|
|
473
|
-
clearAllBtn.classList.remove("hidden");
|
|
474
|
-
} else {
|
|
475
|
-
clearAllBtn.classList.add("hidden");
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function triggerLabelText(key: string, f: AppsFilters): string {
|
|
481
|
-
const map: Record<string, string> = {
|
|
482
|
-
stacks: "Stack",
|
|
483
|
-
platforms: "Platform",
|
|
484
|
-
categories: "Category",
|
|
485
|
-
licenses: "License",
|
|
486
|
-
};
|
|
487
|
-
const base = map[key] ?? key;
|
|
488
|
-
const arr = (f as unknown as Record<string, string[] | undefined>)[key] ?? [];
|
|
489
|
-
if (arr.length === 0) return `${base}: Any`;
|
|
490
|
-
if (arr.length === 1) return `${base}: ${arr[0]}`;
|
|
491
|
-
return `${base}: ${arr.length} selected`;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function applyRowFilter(f: AppsFilters): number {
|
|
495
|
-
const q = f.q?.trim().toLowerCase() ?? "";
|
|
496
|
-
const stacks = new Set(f.stacks ?? []);
|
|
497
|
-
const platforms = new Set(f.platforms ?? []);
|
|
498
|
-
const categories = new Set(f.categories ?? []);
|
|
499
|
-
const labels = new Set(f.labels ?? []);
|
|
500
|
-
const licenses = new Set(f.licenses ?? []);
|
|
501
|
-
const statuses = new Set(f.statuses ?? []);
|
|
502
|
-
|
|
503
|
-
let visible = 0;
|
|
504
|
-
for (const row of rows) {
|
|
505
|
-
const d = row.dataset;
|
|
506
|
-
const rowStacks = (d.stacks || "").split("|").filter(Boolean);
|
|
507
|
-
const rowPlatforms = (d.platforms || "").split("|").filter(Boolean);
|
|
508
|
-
const rowLabels = (d.labels || "").split("|").filter(Boolean);
|
|
509
|
-
|
|
510
|
-
let show = true;
|
|
511
|
-
if (q) {
|
|
512
|
-
// Match the server-side `filterApps` haystack: name, owner,
|
|
513
|
-
// description, category. Without `owner` here, live search
|
|
514
|
-
// would miss apps a user is searching for by owner name.
|
|
515
|
-
const hay = `${d.name ?? ""} ${d.owner ?? ""} ${d.q ?? ""} ${d.category ?? ""}`;
|
|
516
|
-
if (!hay.includes(q)) show = false;
|
|
517
|
-
}
|
|
518
|
-
if (show && stacks.size > 0) {
|
|
519
|
-
if (![d.stack ?? "", ...rowStacks].some((s) => stacks.has(s))) show = false;
|
|
520
|
-
}
|
|
521
|
-
if (show && platforms.size > 0) {
|
|
522
|
-
if (!rowPlatforms.some((p) => platforms.has(p))) show = false;
|
|
523
|
-
}
|
|
524
|
-
if (show && categories.size > 0) {
|
|
525
|
-
if (!categories.has(d.category ?? "")) show = false;
|
|
526
|
-
}
|
|
527
|
-
if (show && labels.size > 0) {
|
|
528
|
-
if (!rowLabels.some((l) => labels.has(l))) show = false;
|
|
529
|
-
}
|
|
530
|
-
if (show && licenses.size > 0) {
|
|
531
|
-
if (!d.license || !licenses.has(d.license)) show = false;
|
|
532
|
-
}
|
|
533
|
-
if (show && statuses.size > 0) {
|
|
534
|
-
if (!d.status || !statuses.has(d.status)) show = false;
|
|
535
|
-
}
|
|
536
|
-
row.hidden = !show;
|
|
537
|
-
if (show) visible++;
|
|
538
|
-
}
|
|
539
|
-
if (resultsList) resultsList.hidden = visible === 0 && rows.length > 0;
|
|
540
|
-
if (emptyState) emptyState.hidden = visible !== 0;
|
|
541
|
-
if (resultsCount) {
|
|
542
|
-
const total = rows.length;
|
|
543
|
-
resultsCount.textContent = `${visible} of ${total}`;
|
|
544
|
-
}
|
|
545
|
-
return visible;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function renderSummary(f: AppsFilters) {
|
|
549
|
-
if (!filtersSummary) return;
|
|
550
|
-
const parts: string[] = [];
|
|
551
|
-
if (f.q) parts.push(`matching “${f.q}”`);
|
|
552
|
-
if (f.stacks?.length) parts.push(`stack: ${f.stacks.join(", ")}`);
|
|
553
|
-
if (f.platforms?.length) parts.push(`platform: ${f.platforms.join(", ")}`);
|
|
554
|
-
if (f.categories?.length) parts.push(`category: ${f.categories.join(", ")}`);
|
|
555
|
-
if (f.licenses?.length) parts.push(`license: ${f.licenses.join(", ")}`);
|
|
556
|
-
if (f.labels?.length) parts.push(`label: ${f.labels.join(", ")}`);
|
|
557
|
-
filtersSummary.textContent = parts.length ? parts.join(" · ") : "";
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function applyAll(f: AppsFilters) {
|
|
561
|
-
applyRowFilter(f);
|
|
562
|
-
renderChips(f);
|
|
563
|
-
renderSummary(f);
|
|
564
|
-
for (const [key, el] of triggerLabels) {
|
|
565
|
-
el.textContent = triggerLabelText(key, f);
|
|
566
|
-
}
|
|
567
|
-
if (searchInput && (searchInput.value ?? "") !== (f.q ?? "")) {
|
|
568
|
-
searchInput.value = f.q ?? "";
|
|
569
|
-
}
|
|
570
|
-
if (sortSelect && f.sort && sortSelect.value !== f.sort) {
|
|
571
|
-
sortSelect.value = f.sort;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Initial render from URL.
|
|
576
|
-
applyAll(readFilters());
|
|
577
|
-
|
|
578
|
-
// Hook called by RefinePanel: patch the filter state, push the
|
|
579
|
-
// new URL, re-filter rows.
|
|
580
|
-
(window as unknown as { __applyFilters: (p: Record<string, string[] | null>) => void }).__applyFilters =
|
|
581
|
-
(patch) => {
|
|
582
|
-
const f = readFilters();
|
|
583
|
-
for (const [key, val] of Object.entries(patch)) {
|
|
584
|
-
if (val === null) {
|
|
585
|
-
(f as unknown as Record<string, unknown>)[key] = undefined;
|
|
586
|
-
} else {
|
|
587
|
-
(f as unknown as Record<string, unknown>)[key] = val;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
f.page = 1;
|
|
591
|
-
const sp = searchParamsFromFilters(f);
|
|
592
|
-
const qs = sp.toString();
|
|
593
|
-
const url = qs ? `?${qs}` : location.pathname;
|
|
594
|
-
history.pushState({ filters: f }, "", url);
|
|
595
|
-
applyAll(f);
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
// Sort dropdown: navigate to new URL (server-side render).
|
|
599
|
-
sortSelect?.addEventListener("change", () => {
|
|
600
|
-
const f = readFilters();
|
|
601
|
-
f.sort = (sortSelect.value as AppsFilters["sort"]) || undefined;
|
|
602
|
-
f.page = 1;
|
|
603
|
-
writeFilters(f);
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
// Active chip removal (delegated) — also works on chips rendered
|
|
607
|
-
// by the client-side filter above.
|
|
608
|
-
document.addEventListener("click", (e) => {
|
|
609
|
-
const target = e.target as HTMLElement;
|
|
610
|
-
const removeBtn = target.closest<HTMLElement>(".apps-active-chip");
|
|
611
|
-
if (removeBtn) {
|
|
612
|
-
e.preventDefault();
|
|
613
|
-
const key = removeBtn.dataset.removeKey as keyof AppsFilters;
|
|
614
|
-
(window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
|
|
615
|
-
[key]: null,
|
|
616
|
-
});
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
// Density toggle: behave like a real <button> — anchor was a
|
|
620
|
-
// misuse of `aria-pressed` (which belongs on buttons). Now
|
|
621
|
-
// we navigate to the URL on click instead of using <a href>.
|
|
622
|
-
const densityBtn = target.closest<HTMLButtonElement>("[data-density-toggle]");
|
|
623
|
-
if (densityBtn) {
|
|
624
|
-
const href = densityBtn.dataset.href;
|
|
625
|
-
if (href) location.assign(href);
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
// Search input: debounced client-side patch (no reload).
|
|
630
|
-
if (searchInput) {
|
|
631
|
-
let timer: number | undefined;
|
|
632
|
-
searchInput.addEventListener("input", () => {
|
|
633
|
-
if (timer) window.clearTimeout(timer);
|
|
634
|
-
timer = window.setTimeout(() => {
|
|
635
|
-
(window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
|
|
636
|
-
q: searchInput.value.trim() ? [searchInput.value.trim()] : null,
|
|
637
|
-
});
|
|
638
|
-
}, 220);
|
|
639
|
-
});
|
|
640
|
-
document.getElementById("search-form")?.addEventListener("submit", (e) => {
|
|
641
|
-
e.preventDefault();
|
|
642
|
-
(window as unknown as { __applyFilters?: (p: Record<string, string[] | null>) => void }).__applyFilters?.({
|
|
643
|
-
q: searchInput.value.trim() ? [searchInput.value.trim()] : null,
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Back/forward: re-apply the filter from the new URL.
|
|
649
|
-
window.addEventListener("popstate", () => {
|
|
650
|
-
applyAll(readFilters());
|
|
651
|
-
});
|
|
652
|
-
</script>
|
|
653
|
-
|
|
654
|
-
<style>
|
|
655
|
-
/* Compact density: hide description + chips, keep header /
|
|
656
|
-
metadata / actions. Toggle is a single class on the parent
|
|
657
|
-
container, so we can flip density without re-rendering rows. */
|
|
658
|
-
.apps-density-compact :global(.apps-row) {
|
|
659
|
-
padding: 0.625rem 0.875rem;
|
|
660
|
-
gap: 0.5rem;
|
|
661
|
-
}
|
|
662
|
-
.apps-density-compact :global(.apps-row-desc) {
|
|
663
|
-
display: none;
|
|
664
|
-
}
|
|
665
|
-
.apps-density-compact :global(.apps-row-chips) {
|
|
666
|
-
display: none;
|
|
667
|
-
}
|
|
668
|
-
</style>
|
|
669
|
-
</BaseLayout>
|