@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
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* List page — the discovery / browse page.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors the openapps list structure:
|
|
6
|
-
* - SmartLensTabs (All / Trending / Recently added / Established / Featured)
|
|
7
|
-
* - RefinePanel (Stack / Platform / Category / License / Sort)
|
|
8
|
-
* - RecordSection rows (full record cards; the `ItemSection` V0
|
|
9
|
-
* name was renamed to `RecordSection` in V1)
|
|
10
|
-
*
|
|
11
|
-
* Generic: the URL slug (e.g. `/projects/`, `/resources/`,
|
|
12
|
-
* `/entities/`) is derived from the blueprint via `getStaticPaths`,
|
|
13
|
-
* so the same page works for every blueprint without per-blueprint
|
|
14
|
-
* forks.
|
|
15
|
-
*
|
|
16
|
-
* Navigation model: full-page render. The form posts via `method="get"`
|
|
17
|
-
* so the browser navigates to `?q=...&sort=...&stack=...`, and the
|
|
18
|
-
* server re-runs `filterRecords` / `applySort` / `paginate` from
|
|
19
|
-
* those query params. No client-side filter shim, no duplicated
|
|
20
|
-
* logic. The URL stays the single source of truth for filter state.
|
|
21
|
-
*/
|
|
22
|
-
import siteConfig from "../../../data/generated/site-config.json";
|
|
23
|
-
import {
|
|
24
|
-
items,
|
|
25
|
-
itemLabel,
|
|
26
|
-
itemLabelPlural,
|
|
27
|
-
taxonomy,
|
|
28
|
-
taxonomyLabel,
|
|
29
|
-
} from "../../data/records";
|
|
30
|
-
import type { IndexFilters } from "@grove-dev/astro";
|
|
31
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
32
|
-
import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
|
|
33
|
-
import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
|
|
34
|
-
import IndexRow from "@grove-dev/astro/components/IndexRow.astro";
|
|
35
|
-
import Pagination from "@grove-dev/astro/components/Pagination.astro";
|
|
36
|
-
import {
|
|
37
|
-
activeFilterChips,
|
|
38
|
-
applySort,
|
|
39
|
-
buildFacets,
|
|
40
|
-
effectivePage,
|
|
41
|
-
effectiveSort,
|
|
42
|
-
filterRecords,
|
|
43
|
-
filtersFromSearchParams,
|
|
44
|
-
searchParamsFromFilters,
|
|
45
|
-
sortDisplay,
|
|
46
|
-
SORT_OPTIONS,
|
|
47
|
-
totalPages,
|
|
48
|
-
} from "@grove-dev/astro";
|
|
49
|
-
|
|
50
|
-
export function getStaticPaths() {
|
|
51
|
-
const routeSlug = siteConfig.blueprintConfig?.routeSlug ?? "projects";
|
|
52
|
-
return [{ params: { slug: routeSlug } }];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const slug = Astro.params.slug as string;
|
|
56
|
-
const total = items.length;
|
|
57
|
-
const filters = filtersFromSearchParams(Astro.url.searchParams);
|
|
58
|
-
function labelFacetsWithTaxonomy(facets: ReturnType<typeof buildFacets>) {
|
|
59
|
-
return {
|
|
60
|
-
...facets,
|
|
61
|
-
stacks: facets.stacks.map((option) => ({
|
|
62
|
-
...option,
|
|
63
|
-
label: taxonomyLabel("stacks", option.value),
|
|
64
|
-
})),
|
|
65
|
-
platforms: facets.platforms.map((option) => ({
|
|
66
|
-
...option,
|
|
67
|
-
label: taxonomyLabel("platforms", option.value),
|
|
68
|
-
})),
|
|
69
|
-
categories: facets.categories.map((option) => ({
|
|
70
|
-
...option,
|
|
71
|
-
label: taxonomyLabel("categories", option.value),
|
|
72
|
-
})),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const facets = labelFacetsWithTaxonomy(buildFacets(items));
|
|
76
|
-
const sort = effectiveSort(filters);
|
|
77
|
-
const requestedPage = effectivePage(filters);
|
|
78
|
-
const filtered = filterRecords(items, filters);
|
|
79
|
-
const sorted = applySort(filtered, sort);
|
|
80
|
-
const pages = totalPages(sorted.length);
|
|
81
|
-
const page = Math.min(requestedPage, pages);
|
|
82
|
-
const chips = activeFilterChips(filters);
|
|
83
|
-
const clientItemsJson = JSON.stringify(items).replace(/</g, "\\u003c");
|
|
84
|
-
|
|
85
|
-
const title = `Browse ${slug} — ${siteConfig.name}`;
|
|
86
|
-
const description = `A searchable directory of ${itemLabelPlural()} for ${siteConfig.name}.`;
|
|
87
|
-
|
|
88
|
-
function hrefForPage(target: number): string {
|
|
89
|
-
const next: IndexFilters = { ...filters, page: target };
|
|
90
|
-
const query = searchParamsFromFilters(next).toString();
|
|
91
|
-
return query ? `/${slug}?${query}` : `/${slug}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
95
|
-
const params = new URLSearchParams();
|
|
96
|
-
const urlKey: Partial<Record<keyof IndexFilters, string>> = {
|
|
97
|
-
stacks: "stack",
|
|
98
|
-
platforms: "platform",
|
|
99
|
-
categories: "category",
|
|
100
|
-
labels: "label",
|
|
101
|
-
licenses: "license",
|
|
102
|
-
statuses: "status",
|
|
103
|
-
lens: "lens",
|
|
104
|
-
q: "q",
|
|
105
|
-
};
|
|
106
|
-
const target = urlKey[key];
|
|
107
|
-
if (target === "status") {
|
|
108
|
-
const next = (params.get("status") ?? "")
|
|
109
|
-
.split(",")
|
|
110
|
-
.filter(Boolean)
|
|
111
|
-
.filter((entry) => entry !== value);
|
|
112
|
-
params.delete("status");
|
|
113
|
-
if (next.length) params.set("status", next.join(","));
|
|
114
|
-
} else if (target) {
|
|
115
|
-
const values = params.getAll(target).filter((entry) => entry !== value);
|
|
116
|
-
params.delete(target);
|
|
117
|
-
for (const entry of values) params.append(target, entry);
|
|
118
|
-
}
|
|
119
|
-
const query = params.toString();
|
|
120
|
-
return query ? `/${slug}?${query}` : `/${slug}`;
|
|
121
|
-
}
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
<BaseLayout
|
|
125
|
-
title={title}
|
|
126
|
-
description={description}
|
|
127
|
-
site={siteConfig}
|
|
128
|
-
submitHref="/submit"
|
|
129
|
-
submitLabel={`Submit ${itemLabel()}`}
|
|
130
|
-
>
|
|
131
|
-
<section class="border-b border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950">
|
|
132
|
-
<div class="mx-auto w-full max-w-wide px-5 pt-10 sm:px-7 sm:pt-12">
|
|
133
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
134
|
-
{slug}
|
|
135
|
-
</span>
|
|
136
|
-
<h1 class="m-0 mt-1 text-[1.875rem] sm:text-[2.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
137
|
-
Browse {slug}
|
|
138
|
-
</h1>
|
|
139
|
-
<p class="m-0 mt-2 max-w-[64ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
140
|
-
Search, filter, and explore {total} {itemLabel()}{total === 1 ? "" : "s"} in this directory.
|
|
141
|
-
</p>
|
|
142
|
-
|
|
143
|
-
<form
|
|
144
|
-
method="get"
|
|
145
|
-
action={`/${slug}`}
|
|
146
|
-
id="search-form"
|
|
147
|
-
class="mt-6 flex flex-col gap-2 sm:flex-row"
|
|
148
|
-
role="search"
|
|
149
|
-
>
|
|
150
|
-
<label class="relative flex-1">
|
|
151
|
-
<span class="sr-only">{`Search ${itemLabelPlural()}`}</span>
|
|
152
|
-
<svg
|
|
153
|
-
viewBox="0 0 16 16"
|
|
154
|
-
width="16"
|
|
155
|
-
height="16"
|
|
156
|
-
aria-hidden="true"
|
|
157
|
-
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
|
|
158
|
-
fill="currentColor"
|
|
159
|
-
>
|
|
160
|
-
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z" />
|
|
161
|
-
</svg>
|
|
162
|
-
<input
|
|
163
|
-
id="search-input"
|
|
164
|
-
type="search"
|
|
165
|
-
name="q"
|
|
166
|
-
value={filters.q ?? ""}
|
|
167
|
-
placeholder={`Search ${itemLabelPlural()}, stacks, categories, platforms, licenses...`}
|
|
168
|
-
class="h-10 w-full rounded-md border border-ink-200 bg-white py-2 pl-9 pr-3 text-sm text-ink-900 placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
|
|
169
|
-
/>
|
|
170
|
-
</label>
|
|
171
|
-
<label class="relative">
|
|
172
|
-
<span class="sr-only">{`Sort ${itemLabelPlural()}`}</span>
|
|
173
|
-
<select
|
|
174
|
-
id="sort-select"
|
|
175
|
-
name="sort"
|
|
176
|
-
class="h-10 cursor-pointer appearance-none rounded-md border border-ink-200 bg-white py-1.5 pl-3 pr-8 text-sm font-medium text-ink-700 transition-colors hover:border-ink-300 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
|
|
177
|
-
>
|
|
178
|
-
{SORT_OPTIONS.map((o) => (
|
|
179
|
-
<option value={o.value} selected={o.value === sort}>{o.label}</option>
|
|
180
|
-
))}
|
|
181
|
-
</select>
|
|
182
|
-
<svg
|
|
183
|
-
viewBox="0 0 16 16"
|
|
184
|
-
width="11"
|
|
185
|
-
height="11"
|
|
186
|
-
aria-hidden="true"
|
|
187
|
-
class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
188
|
-
fill="currentColor"
|
|
189
|
-
>
|
|
190
|
-
<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" />
|
|
191
|
-
</svg>
|
|
192
|
-
</label>
|
|
193
|
-
<button type="submit" 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">Search</button>
|
|
194
|
-
</form>
|
|
195
|
-
|
|
196
|
-
<div class="mt-4">
|
|
197
|
-
<SmartLensTabs
|
|
198
|
-
currentParams={Astro.url.searchParams}
|
|
199
|
-
pathPrefix={`/${slug}`}
|
|
200
|
-
lensIds={["all", "new", "hot", "mature", "production-like", "good-to-learn"]}
|
|
201
|
-
/>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
<div class="mt-3 pb-6">
|
|
205
|
-
<RefinePanel
|
|
206
|
-
facets={facets}
|
|
207
|
-
initial={filters}
|
|
208
|
-
pathPrefix={`/${slug}`}
|
|
209
|
-
/>
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
</section>
|
|
213
|
-
|
|
214
|
-
<section class="py-10 sm:py-12">
|
|
215
|
-
<div class="mx-auto w-full max-w-wide px-5 sm:px-7">
|
|
216
|
-
<header class="mb-6">
|
|
217
|
-
<h2 class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
218
|
-
{`All ${itemLabelPlural()}`}
|
|
219
|
-
</h2>
|
|
220
|
-
<p class="m-0 mt-1 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
221
|
-
{`Every ${itemLabel()} in the directory.`}
|
|
222
|
-
</p>
|
|
223
|
-
</header>
|
|
224
|
-
<div class="mb-5 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
225
|
-
<p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
|
|
226
|
-
<span id="results-count" class="font-mono text-ink-900 dark:text-ink-100">
|
|
227
|
-
{sorted.length} {sorted.length === 1 ? itemLabel() : itemLabelPlural()}
|
|
228
|
-
</span>
|
|
229
|
-
<span id="results-sort">{` · sorted by ${sortDisplay(sort)}`}</span>
|
|
230
|
-
<span id="results-page">{pages > 1 ? ` · page ${page} of ${pages}` : ""}</span>
|
|
231
|
-
</p>
|
|
232
|
-
<div
|
|
233
|
-
id="active-filters"
|
|
234
|
-
class:list={["flex flex-wrap items-center gap-1.5", chips.length === 0 && "hidden"]}
|
|
235
|
-
aria-label="Active filters"
|
|
236
|
-
>
|
|
237
|
-
{chips.length > 0 && (
|
|
238
|
-
<>
|
|
239
|
-
{chips.map((chip) => (
|
|
240
|
-
<a
|
|
241
|
-
href={hrefForRemove(chip.key, chip.value)}
|
|
242
|
-
class="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"
|
|
243
|
-
>
|
|
244
|
-
{chip.label} <span aria-hidden="true">x</span>
|
|
245
|
-
</a>
|
|
246
|
-
))}
|
|
247
|
-
<a href={`/${slug}`} class="text-2xs font-medium text-ink-500 hover:text-ink-900">
|
|
248
|
-
Clear all
|
|
249
|
-
</a>
|
|
250
|
-
</>
|
|
251
|
-
)}
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
<ul id="results-list" class="m-0 flex list-none flex-col gap-3 p-0">
|
|
256
|
-
{sorted.map((item, i) => (
|
|
257
|
-
<li class="m-0 p-0" data-record-slug={item.slug}>
|
|
258
|
-
<IndexRow
|
|
259
|
-
record={item as Parameters<typeof IndexRow>[0]["record"]}
|
|
260
|
-
detailHref={`/${slug}/${item.slug}`}
|
|
261
|
-
eager={i < 2}
|
|
262
|
-
/>
|
|
263
|
-
</li>
|
|
264
|
-
))}
|
|
265
|
-
</ul>
|
|
266
|
-
<div id="empty-state" class:list={["py-12 text-center", sorted.length > 0 && "hidden"]}>
|
|
267
|
-
<p class="m-0 text-sm font-medium text-ink-700 dark:text-ink-200">
|
|
268
|
-
{`No ${itemLabel()}s match these filters.`}
|
|
269
|
-
</p>
|
|
270
|
-
<a href={`/${slug}`} 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 mt-4">Clear filters</a>
|
|
271
|
-
</div>
|
|
272
|
-
<nav id="results-pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" aria-label="Pagination">
|
|
273
|
-
<Pagination current={page} total={pages} hrefFor={hrefForPage} />
|
|
274
|
-
</nav>
|
|
275
|
-
</div>
|
|
276
|
-
</section>
|
|
277
|
-
|
|
278
|
-
<section class="border-t border-ink-200 dark:border-ink-800">
|
|
279
|
-
<div class="mx-auto w-full max-w-wide px-5 sm:px-7">
|
|
280
|
-
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
|
281
|
-
<h2 class="m-0 text-xl font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
282
|
-
{`Know a real ${itemLabel()} that belongs here?`}
|
|
283
|
-
</h2>
|
|
284
|
-
<p class="m-0 max-w-2xl text-sm text-ink-500 dark:text-ink-400">
|
|
285
|
-
{`Open a pull request with a YAML file. We review every submission against the bar — no marketing, no tutorials.`}
|
|
286
|
-
</p>
|
|
287
|
-
<a
|
|
288
|
-
href="/submit"
|
|
289
|
-
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 mt-2"
|
|
290
|
-
>
|
|
291
|
-
{`Submit ${itemLabel()}`}
|
|
292
|
-
</a>
|
|
293
|
-
|
|
294
|
-
</div>
|
|
295
|
-
</div>
|
|
296
|
-
</section>
|
|
297
|
-
|
|
298
|
-
<script is:inline id="grove-index-data" type="application/json" set:html={clientItemsJson}></script>
|
|
299
|
-
<script
|
|
300
|
-
is:inline
|
|
301
|
-
define:vars={{
|
|
302
|
-
slug,
|
|
303
|
-
singular: itemLabel(),
|
|
304
|
-
plural: itemLabelPlural(),
|
|
305
|
-
taxonomy,
|
|
306
|
-
}}
|
|
307
|
-
>
|
|
308
|
-
(() => {
|
|
309
|
-
const PAGE_SIZE = 20;
|
|
310
|
-
const source = document.querySelector("#grove-index-data");
|
|
311
|
-
const list = document.querySelector("#results-list");
|
|
312
|
-
if (!source || !list) return;
|
|
313
|
-
|
|
314
|
-
const items = JSON.parse(source.textContent || "[]");
|
|
315
|
-
const nodes = new Map(
|
|
316
|
-
[...list.querySelectorAll("[data-record-slug]")].map((node) => [
|
|
317
|
-
node.dataset.recordSlug,
|
|
318
|
-
node,
|
|
319
|
-
]),
|
|
320
|
-
);
|
|
321
|
-
const params = new URLSearchParams(location.search);
|
|
322
|
-
const values = (key) => params.getAll(key).filter(Boolean);
|
|
323
|
-
const q = (params.get("q") || "").trim().toLowerCase();
|
|
324
|
-
const stacks = values("stack");
|
|
325
|
-
const platforms = values("platform");
|
|
326
|
-
const categories = values("category");
|
|
327
|
-
const labels = values("label");
|
|
328
|
-
const licenses = values("license");
|
|
329
|
-
const statuses = values("status").flatMap((value) => value.split(",")).filter(Boolean);
|
|
330
|
-
const lens = params.get("lens") || "";
|
|
331
|
-
const sort = params.get("sort") || "recently-updated";
|
|
332
|
-
const requestedPage = Math.max(1, Number.parseInt(params.get("page") || "1", 10) || 1);
|
|
333
|
-
|
|
334
|
-
const nameOf = (record) => record.kind === "resource" ? record.title : record.name;
|
|
335
|
-
const projectValues = (record) => record.kind === "project"
|
|
336
|
-
? [record.stack, ...(record.stacks || [])].filter(Boolean)
|
|
337
|
-
: [];
|
|
338
|
-
const matchesAny = (actual, expected) =>
|
|
339
|
-
expected.length === 0 || actual.some((value) => expected.includes(value));
|
|
340
|
-
const matches = (record) => {
|
|
341
|
-
if (q) {
|
|
342
|
-
const repo = record.kind === "project"
|
|
343
|
-
? record.repoUrl || record.links?.github || ""
|
|
344
|
-
: record.links?.github || "";
|
|
345
|
-
const owner = /github\.com\/([^/]+)\//.exec(repo)?.[1] || "";
|
|
346
|
-
const projectFields = record.kind === "project"
|
|
347
|
-
? [
|
|
348
|
-
record.stack,
|
|
349
|
-
...(record.stacks || []),
|
|
350
|
-
...(record.platforms || []),
|
|
351
|
-
record.projectType,
|
|
352
|
-
...(record.bestFor || []),
|
|
353
|
-
...(record.whyListed || []),
|
|
354
|
-
record.github?.license,
|
|
355
|
-
record.health?.status,
|
|
356
|
-
]
|
|
357
|
-
: [];
|
|
358
|
-
const haystack = [
|
|
359
|
-
nameOf(record),
|
|
360
|
-
owner,
|
|
361
|
-
record.description,
|
|
362
|
-
record.category,
|
|
363
|
-
...(record.tags || []),
|
|
364
|
-
...projectFields,
|
|
365
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
366
|
-
if (!haystack.includes(q)) return false;
|
|
367
|
-
}
|
|
368
|
-
if (!matchesAny(projectValues(record), stacks)) return false;
|
|
369
|
-
if (!matchesAny(record.kind === "project" ? record.platforms || [] : [], platforms)) return false;
|
|
370
|
-
if (categories.length && !categories.includes(record.category)) return false;
|
|
371
|
-
if (!matchesAny(record.curation?.labels || [], labels)) return false;
|
|
372
|
-
if (licenses.length && !licenses.includes(record.kind === "project" ? record.github?.license : "")) return false;
|
|
373
|
-
if (statuses.length && !statuses.includes(record.kind === "project" ? record.health?.status : "")) return false;
|
|
374
|
-
if (lens && !(record.curation?.lenses || []).includes(lens)) return false;
|
|
375
|
-
return true;
|
|
376
|
-
};
|
|
377
|
-
const time = (value) => value ? new Date(value).valueOf() || 0 : 0;
|
|
378
|
-
const stars = (record) => record.kind === "project" ? record.github?.stars || 0 : 0;
|
|
379
|
-
const compare = {
|
|
380
|
-
"most-starred": (a, b) => stars(b) - stars(a),
|
|
381
|
-
"recently-updated": (a, b) =>
|
|
382
|
-
time(b.kind === "project" ? b.github?.pushedAt : b.publishedAt) -
|
|
383
|
-
time(a.kind === "project" ? a.github?.pushedAt : a.publishedAt),
|
|
384
|
-
"recently-added": (a, b) =>
|
|
385
|
-
time(b.curation?.reviewedAt) - time(a.curation?.reviewedAt),
|
|
386
|
-
"best-overall": (a, b) =>
|
|
387
|
-
Number(Boolean(b.curation?.reviewed)) - Number(Boolean(a.curation?.reviewed)) ||
|
|
388
|
-
stars(b) - stars(a),
|
|
389
|
-
"alphabetical": (a, b) => String(nameOf(a)).localeCompare(String(nameOf(b))),
|
|
390
|
-
}[sort];
|
|
391
|
-
|
|
392
|
-
function applyClientFilters() {
|
|
393
|
-
const filtered = items.filter(matches);
|
|
394
|
-
if (compare) filtered.sort(compare);
|
|
395
|
-
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
|
396
|
-
const page = Math.min(requestedPage, pageCount);
|
|
397
|
-
const visible = new Set(
|
|
398
|
-
filtered
|
|
399
|
-
.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
|
400
|
-
.map((record) => record.slug),
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
for (const record of filtered) {
|
|
404
|
-
const node = nodes.get(record.slug);
|
|
405
|
-
if (node) list.append(node);
|
|
406
|
-
}
|
|
407
|
-
for (const [recordSlug, node] of nodes) {
|
|
408
|
-
node.hidden = !visible.has(recordSlug);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const count = document.querySelector("#results-count");
|
|
412
|
-
const pageLabel = document.querySelector("#results-page");
|
|
413
|
-
const empty = document.querySelector("#empty-state");
|
|
414
|
-
if (count) count.textContent = `${filtered.length} ${filtered.length === 1 ? singular : plural}`;
|
|
415
|
-
if (pageLabel) pageLabel.textContent = pageCount > 1 ? ` · page ${page} of ${pageCount}` : "";
|
|
416
|
-
if (empty) empty.classList.toggle("hidden", filtered.length > 0);
|
|
417
|
-
|
|
418
|
-
const pagination = document.querySelector("#results-pagination");
|
|
419
|
-
if (pagination) {
|
|
420
|
-
pagination.innerHTML = "";
|
|
421
|
-
if (pageCount > 1) {
|
|
422
|
-
for (let target = 1; target <= pageCount; target += 1) {
|
|
423
|
-
const next = new URLSearchParams(params);
|
|
424
|
-
if (target === 1) next.delete("page");
|
|
425
|
-
else next.set("page", String(target));
|
|
426
|
-
const link = document.createElement("a");
|
|
427
|
-
link.href = `/${slug}${next.size ? `?${next}` : ""}`;
|
|
428
|
-
link.textContent = String(target);
|
|
429
|
-
link.setAttribute("aria-label", `Page ${target}`);
|
|
430
|
-
if (target === page) link.setAttribute("aria-current", "page");
|
|
431
|
-
link.className = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border px-2 text-sm";
|
|
432
|
-
pagination.append(link);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const searchInput = document.querySelector("#search-input");
|
|
439
|
-
if (searchInput) searchInput.value = params.get("q") || "";
|
|
440
|
-
document.querySelectorAll('select[name="sort"]').forEach((select) => {
|
|
441
|
-
select.value = sort;
|
|
442
|
-
});
|
|
443
|
-
const filterKeys = {
|
|
444
|
-
stacks: "stack",
|
|
445
|
-
platforms: "platform",
|
|
446
|
-
categories: "category",
|
|
447
|
-
licenses: "license",
|
|
448
|
-
};
|
|
449
|
-
const filterLabels = {
|
|
450
|
-
stacks: "Stack",
|
|
451
|
-
platforms: "Platform",
|
|
452
|
-
categories: "Category",
|
|
453
|
-
licenses: "License",
|
|
454
|
-
};
|
|
455
|
-
const taxonomyKinds = {
|
|
456
|
-
stacks: "stacks",
|
|
457
|
-
platforms: "platforms",
|
|
458
|
-
categories: "categories",
|
|
459
|
-
};
|
|
460
|
-
const taxonomyName = (groupKey, value) =>
|
|
461
|
-
(taxonomy[taxonomyKinds[groupKey]] || []).find((entry) => entry.id === value)?.name ??
|
|
462
|
-
value;
|
|
463
|
-
document.querySelectorAll(".grove-filter").forEach((group) => {
|
|
464
|
-
const selected = values(filterKeys[group.dataset.filterGroupKey]);
|
|
465
|
-
group.querySelectorAll(".grove-filter-input").forEach((input) => {
|
|
466
|
-
input.checked = selected.includes(input.dataset.filterValue);
|
|
467
|
-
});
|
|
468
|
-
const trigger = group.querySelector(".grove-filter-trigger span");
|
|
469
|
-
if (trigger && selected.length) {
|
|
470
|
-
const label = filterLabels[group.dataset.filterGroupKey];
|
|
471
|
-
trigger.textContent = `${label}: ${
|
|
472
|
-
selected.length === 1
|
|
473
|
-
? taxonomyName(group.dataset.filterGroupKey, selected[0])
|
|
474
|
-
: `${selected.length} selected`
|
|
475
|
-
}`;
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
applyClientFilters();
|
|
480
|
-
})();
|
|
481
|
-
</script>
|
|
482
|
-
</BaseLayout>
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* About page — generic.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors the openapps section structure: breadcrumb, header with
|
|
6
|
-
* two-line intro, then a stack of cards (Why, Value, Data, Generic)
|
|
7
|
-
* followed by a CTA row. The copy is generic — references "directory",
|
|
8
|
-
* "record", "framework" — so it works for any blueprint.
|
|
9
|
-
*
|
|
10
|
-
* Side-by-side comparison with the older "Why" copy:
|
|
11
|
-
* - Old: "A directory of real open-source apps. Started from
|
|
12
|
-
* the Open Source Flutter Apps collection…"
|
|
13
|
-
* - New: "A growing community knowledge site. A curated, health-
|
|
14
|
-
* aware developer directory…"
|
|
15
|
-
*
|
|
16
|
-
* The Tailwind card and button utilities match the
|
|
17
|
-
* rest of the framework, and the data fields it references
|
|
18
|
-
* (`siteConfig.tagline`, `siteConfig.repoUrl`) all flow from the
|
|
19
|
-
* site config — no hardcoded brand or copy.
|
|
20
|
-
*/
|
|
21
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
22
|
-
import Container from "@grove-dev/astro/layouts/Container.astro";
|
|
23
|
-
import siteConfig from "../../data/generated/site-config.json";
|
|
24
|
-
import { getPageContentHtml, indexSlug, itemLabel } from "../data/records";
|
|
25
|
-
|
|
26
|
-
const githubUrl = siteConfig.repoUrl;
|
|
27
|
-
const title = `About — ${siteConfig.name}`;
|
|
28
|
-
const description = `${siteConfig.tagline} Built with Grove.`;
|
|
29
|
-
const slug = indexSlug();
|
|
30
|
-
const item = itemLabel();
|
|
31
|
-
const pageContentHtml = getPageContentHtml("about");
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
35
|
-
<Container size="narrow">
|
|
36
|
-
<article class="py-12 sm:py-16">
|
|
37
|
-
<nav class="text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
38
|
-
<a href="/" class="hover:text-ink-900 dark:hover:text-ink-100">Home</a>
|
|
39
|
-
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
40
|
-
<span class="text-ink-700 dark:text-ink-300">About</span>
|
|
41
|
-
</nav>
|
|
42
|
-
|
|
43
|
-
{pageContentHtml ? (
|
|
44
|
-
<section class="grove-prose mt-8" set:html={pageContentHtml} />
|
|
45
|
-
) : (
|
|
46
|
-
<>
|
|
47
|
-
<header class="mt-5">
|
|
48
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
49
|
-
About
|
|
50
|
-
</span>
|
|
51
|
-
<h1 class="mt-1 m-0 text-[1.75rem] sm:text-[2.25rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100">
|
|
52
|
-
A growing community knowledge site.
|
|
53
|
-
</h1>
|
|
54
|
-
<p class="mt-4 m-0 text-[1rem] sm:text-[1.0625rem] leading-relaxed text-ink-700 dark:text-ink-200 max-w-[62ch]">
|
|
55
|
-
{siteConfig.name} is a curated, health-aware developer directory —
|
|
56
|
-
built so you can find, compare, run, study, and contribute to
|
|
57
|
-
open-source {item}s.
|
|
58
|
-
</p>
|
|
59
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 max-w-[62ch]">
|
|
60
|
-
A growing list, forked from the original. Same shape, same
|
|
61
|
-
spirit, fresh data.
|
|
62
|
-
</p>
|
|
63
|
-
</header>
|
|
64
|
-
|
|
65
|
-
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-8 p-5 sm:p-6">
|
|
66
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
67
|
-
Why
|
|
68
|
-
</span>
|
|
69
|
-
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
70
|
-
A curated starting point
|
|
71
|
-
</h2>
|
|
72
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
73
|
-
GitHub search and README lists optimize for the repository
|
|
74
|
-
name and description. They rarely tell you whether a project
|
|
75
|
-
is a real {item} or a demo, which stack it uses beyond the
|
|
76
|
-
headline, how active it really is, or whether it has a clear
|
|
77
|
-
license.
|
|
78
|
-
</p>
|
|
79
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
80
|
-
{siteConfig.name} turns that flat list into a structured,
|
|
81
|
-
health-aware index. Each entry carries the metadata a developer
|
|
82
|
-
actually needs to decide whether to open the repo.
|
|
83
|
-
</p>
|
|
84
|
-
</section>
|
|
85
|
-
|
|
86
|
-
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
87
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
88
|
-
Why real {item}s
|
|
89
|
-
</span>
|
|
90
|
-
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
91
|
-
Real, runnable source
|
|
92
|
-
</h2>
|
|
93
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
94
|
-
Every {item} links back to its source. Skim, fork, learn — no
|
|
95
|
-
paywalls, no broken download links.
|
|
96
|
-
</p>
|
|
97
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
98
|
-
The directory is hand-curated — no SEO spam, no affiliate
|
|
99
|
-
farms, no broken links.
|
|
100
|
-
</p>
|
|
101
|
-
</section>
|
|
102
|
-
|
|
103
|
-
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
104
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
105
|
-
What you get
|
|
106
|
-
</span>
|
|
107
|
-
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
108
|
-
What every page gives you
|
|
109
|
-
</h2>
|
|
110
|
-
<ul class="m-0 mt-3 list-none space-y-1.5 p-0 text-[0.9375rem] text-ink-700 dark:text-ink-200">
|
|
111
|
-
<li class="flex items-baseline gap-2">
|
|
112
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
113
|
-
<span>Stack, category, and architecture at a glance</span>
|
|
114
|
-
</li>
|
|
115
|
-
<li class="flex items-baseline gap-2">
|
|
116
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
117
|
-
<span>Supported platforms and license</span>
|
|
118
|
-
</li>
|
|
119
|
-
<li class="flex items-baseline gap-2">
|
|
120
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
121
|
-
<span>Activity and status — last commit, monthly history, health signals</span>
|
|
122
|
-
</li>
|
|
123
|
-
<li class="flex items-baseline gap-2">
|
|
124
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
125
|
-
<span>License, stars, and curation tier</span>
|
|
126
|
-
</li>
|
|
127
|
-
<li class="flex items-baseline gap-2">
|
|
128
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
129
|
-
<span>Curated notes: best for, why listed, caveats</span>
|
|
130
|
-
</li>
|
|
131
|
-
<li class="flex items-baseline gap-2">
|
|
132
|
-
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
133
|
-
<span>Filters that respect you — search by stack, platform, license, or status</span>
|
|
134
|
-
</li>
|
|
135
|
-
</ul>
|
|
136
|
-
</section>
|
|
137
|
-
|
|
138
|
-
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
139
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
140
|
-
Generic by design
|
|
141
|
-
</span>
|
|
142
|
-
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
143
|
-
Generic by design
|
|
144
|
-
</h2>
|
|
145
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
146
|
-
The framework supports three blueprints: project-directory,
|
|
147
|
-
resource-hub, and ecosystem-map. Each can be configured
|
|
148
|
-
without touching the components — pages, schema, and
|
|
149
|
-
routes all derive from <code class="font-mono">grove.config.ts</code>.
|
|
150
|
-
</p>
|
|
151
|
-
</section>
|
|
152
|
-
|
|
153
|
-
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
154
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
155
|
-
Free to fork, free to extend
|
|
156
|
-
</span>
|
|
157
|
-
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
158
|
-
Free to fork, free to extend
|
|
159
|
-
</h2>
|
|
160
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
161
|
-
The full data lives in this repository as plain YAML. The
|
|
162
|
-
static output is plain HTML. No database, no JavaScript
|
|
163
|
-
framework, no CDN with sticky sessions.
|
|
164
|
-
</p>
|
|
165
|
-
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
166
|
-
Anyone can suggest a new {item}, fix a typo, or improve the
|
|
167
|
-
metadata through a pull request.
|
|
168
|
-
</p>
|
|
169
|
-
</section>
|
|
170
|
-
|
|
171
|
-
<section class="mt-8 flex flex-wrap items-center gap-3">
|
|
172
|
-
<a href={`/${slug}`} 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">Browse the directory</a>
|
|
173
|
-
{githubUrl && (
|
|
174
|
-
<a
|
|
175
|
-
href={githubUrl}
|
|
176
|
-
target="_blank"
|
|
177
|
-
rel="noopener noreferrer"
|
|
178
|
-
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"
|
|
179
|
-
>
|
|
180
|
-
View on GitHub
|
|
181
|
-
</a>
|
|
182
|
-
)}
|
|
183
|
-
<a href="/submit" 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-transparent text-foreground hover:bg-accent hover:text-accent-foreground">Submit a {item} →</a>
|
|
184
|
-
</section>
|
|
185
|
-
</>
|
|
186
|
-
)}
|
|
187
|
-
</article>
|
|
188
|
-
</Container>
|
|
189
|
-
</BaseLayout>
|