@grove-dev/astro 0.2.6 → 0.2.13
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/lib/display.d.ts +11 -17
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +43 -43
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +6 -7
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +11 -12
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +35 -73
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +144 -126
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +11 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +12 -25
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +15 -16
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +4 -11
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +47 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +195 -180
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +7 -21
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +12 -31
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +4 -4
- package/src/components/AppsIndexRow.astro +55 -184
- package/src/components/AppsPagination.astro +45 -108
- package/src/components/CategoryGrid.astro +43 -31
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +41 -0
- package/src/components/ExploreByStack.astro +6 -0
- package/src/components/FilterGroupMenu.astro +49 -0
- package/src/components/FilterOptions.astro +44 -0
- package/src/components/Hero.astro +198 -147
- package/src/components/Icon.astro +10 -3
- package/src/components/ItemCard.astro +266 -248
- package/src/components/RecordSection.astro +68 -33
- package/src/components/RefinePanel.astro +115 -141
- package/src/components/StackGrid.astro +32 -24
- package/src/components/WhyThisExists.astro +32 -32
- package/src/layouts/BaseLayout.astro +104 -49
- package/src/layouts/Container.astro +5 -44
- package/src/layouts/Footer.astro +31 -20
- package/src/layouts/Header.astro +40 -19
- package/src/layouts/SectionHeader.astro +27 -43
- package/src/layouts/Seo.astro +38 -56
- package/src/layouts/ThemeToggle.astro +35 -48
- package/src/lib/display.ts +46 -53
- package/src/lib/format.ts +11 -12
- package/src/lib/lenses.ts +151 -172
- package/src/lib/repo.ts +16 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +222 -290
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +207 -1
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
- package/templates/default/.github/pull_request_template.md +27 -0
- package/templates/default/.github/workflows/build.yml +55 -0
- package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
- package/templates/default/.github/workflows/daily-refresh.yml +53 -0
- package/templates/default/.github/workflows/sync-contributors.yml +53 -0
- package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
- package/templates/default/.github/workflows/validate-data.yml +35 -0
- package/templates/default/data/records/coolify.yml +46 -0
- package/templates/default/data/records/gitea.yml +46 -0
- package/templates/default/data/records/hoppscotch.yml +47 -0
- package/templates/default/data/records/inventree.yml +47 -0
- package/templates/default/data/records/label-studio.yml +46 -0
- package/templates/default/data/records/logseq.yml +48 -0
- package/templates/default/data/records/private-gpt.yml +47 -0
- package/templates/default/data/records/wakapi.yml +44 -0
- package/templates/default/data/taxonomy/categories.yml +47 -0
- package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
- package/templates/default/data/taxonomy/platforms.yml +27 -0
- package/templates/default/data/taxonomy/stacks.yml +41 -0
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +5 -4
- package/templates/default/public/icons/stacks/bun.svg +1 -0
- package/templates/default/public/icons/stacks/clojure.svg +1 -0
- package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
- package/templates/default/public/icons/stacks/deno.svg +1 -0
- package/templates/default/public/icons/stacks/django.svg +1 -0
- package/templates/default/public/icons/stacks/docker.svg +1 -0
- package/templates/default/public/icons/stacks/go.svg +1 -0
- package/templates/default/public/icons/stacks/llm.svg +1 -0
- package/templates/default/public/icons/stacks/node.js.svg +1 -0
- package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
- package/templates/default/public/icons/stacks/vue.svg +1 -0
- package/templates/default/public/llms-full.txt +197 -5
- package/templates/default/public/llms.txt +107 -6
- package/templates/default/public/sitemap.xml +63 -0
- package/templates/default/scripts/build-llms.mjs +123 -0
- package/templates/default/scripts/build-records-json.mjs +27 -0
- package/templates/default/scripts/build-sitemap.mjs +20 -0
- package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
- package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
- package/templates/default/scripts/fetch-icons.mjs +137 -0
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
- package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
- package/templates/default/scripts/refresh-records-activity.mjs +24 -0
- package/templates/default/scripts/repair-license-format.mjs +72 -0
- package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
- package/templates/default/scripts/seed-from-github.mjs +62 -0
- package/templates/default/scripts/sync-contributors.mjs +145 -0
- package/templates/default/scripts/sync-github-metadata.mjs +28 -0
- package/templates/default/scripts/validate-records.mjs +27 -0
- package/templates/default/src/data/records.ts +134 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +476 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
- package/templates/default/src/pages/apps/index.astro +146 -0
- package/templates/default/src/pages/contributors.astro +184 -0
- package/templates/default/src/pages/index.astro +119 -162
- package/templates/default/src/pages/submit.astro +276 -114
- package/templates/default/wrangler.jsonc +28 -0
- package/LICENSE +0 -21
- package/templates/default/data/generated/apps.json +0 -1
- package/templates/default/data/generated/records.full.json +0 -8
- package/templates/default/data/generated/records.index.json +0 -7
- package/templates/default/data/generated/records.json +0 -8
- package/templates/default/data/generated/repo-stats.json +0 -1
- package/templates/default/data/generated/site-config.json +0 -31
- package/templates/default/src/components/layout/BaseLayout.astro +0 -47
- package/templates/default/src/components/layout/Container.astro +0 -18
- package/templates/default/src/components/layout/Footer.astro +0 -27
- package/templates/default/src/components/layout/Header.astro +0 -43
- package/templates/default/src/lib/markdown.ts +0 -171
- package/templates/default/src/pages/projects/index.astro +0 -193
|
@@ -1,62 +1,54 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* ItemCard —
|
|
3
|
+
* ItemCard — compact card for one record on the home page (or any
|
|
4
|
+
* 3-column grid).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* V1 (published) API: takes `record` (a Resource / IndexProjectRecord)
|
|
7
|
+
* and `href` (the full detail-page URL). The page that calls this
|
|
8
|
+
* component owns the path layout — blueprint-aware consumers
|
|
9
|
+
* (project-directory → /projects/<slug>, resource-hub →
|
|
10
|
+
* /resources/<slug>, ecosystem-map → /entities/<slug>) pass the
|
|
11
|
+
* correct href via `RecordSection`'s `hrefFor` callback.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - Optional curated mark (label badge) — e.g. "Featured", "New".
|
|
14
|
-
* - Health badge (Active / Mature / Stale / Archived / ...).
|
|
15
|
-
* - License + star count + last-updated date.
|
|
16
|
-
* - Two CTAs: "View repo" (links.github if present, else repoUrl)
|
|
17
|
-
* and "Details" (the page link).
|
|
18
|
-
*
|
|
19
|
-
* Generic: takes any `Resource` (`kind: "project" | "resource" |
|
|
20
|
-
* "entity"`). For "project" we read the project-style fields
|
|
21
|
-
* (`repoUrl`, `logoUrl`, `stacks`, `platforms`, `github`,
|
|
22
|
-
* `curation`, `health`); for other kinds we degrade gracefully to
|
|
23
|
-
* the minimal view.
|
|
24
|
-
*
|
|
25
|
-
* Index-payload-friendly: when the consumer passes an
|
|
26
|
-
* `IndexProjectRecord` (the slim list-page shape), the component
|
|
27
|
-
* reads the projected fields directly — `github.stars` /
|
|
28
|
-
* `github.pushedAt` / `github.license` — rather than the raw
|
|
29
|
-
* `github.repository` block.
|
|
13
|
+
* V1 (legacy) API: also accepts `item` + `pathPrefix` as a
|
|
14
|
+
* convenience for callers that haven't migrated. When both are
|
|
15
|
+
* provided, `href` wins; when only `pathPrefix` is provided, the
|
|
16
|
+
* detail href is `${pathPrefix}/${item.slug}`.
|
|
30
17
|
*/
|
|
31
|
-
import type {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import { formatRelative, formatStars } from "../lib/format";
|
|
39
|
-
import { statusDisplay, prettySlug, labelDisplay } from "../lib/display";
|
|
18
|
+
import type { IndexRecord, Resource, ProjectRecord, IndexProjectRecord } from "@grove-dev/core";
|
|
19
|
+
import Icon from "./Icon.astro";
|
|
20
|
+
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
|
|
21
|
+
import { formatStars, formatRelative } from "@grove-dev/astro";
|
|
22
|
+
import { statusDisplay } from "@grove-dev/astro";
|
|
23
|
+
|
|
24
|
+
type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
|
|
40
25
|
|
|
41
26
|
interface Props {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
/** V1 API — full Resource/IndexProjectRecord. */
|
|
28
|
+
record?: CardRecord;
|
|
29
|
+
/** V1 API — full detail-page URL. */
|
|
30
|
+
href?: string;
|
|
31
|
+
/** Legacy API — singular item, equivalent to `record`. */
|
|
32
|
+
item?: CardRecord;
|
|
33
|
+
/** Legacy API — URL prefix, equivalent to deriving from
|
|
34
|
+
* blueprint routeSlug + slug. */
|
|
35
|
+
pathPrefix?: string;
|
|
45
36
|
/** When true, show owner/repo below the title. Default true. */
|
|
46
37
|
showOwner?: boolean;
|
|
47
|
-
/** When
|
|
38
|
+
/** When false, hide the platform + stack chip row. Default true. */
|
|
48
39
|
showChips?: boolean;
|
|
49
|
-
/** When
|
|
40
|
+
/** When false, hide the curated label. Default true. */
|
|
50
41
|
showLabel?: boolean;
|
|
51
|
-
/** When
|
|
42
|
+
/** When false, hide the stars/updated/license metadata row. */
|
|
52
43
|
showMeta?: boolean;
|
|
53
|
-
/** Extra class on the outer <article>. */
|
|
54
44
|
class?: string;
|
|
55
45
|
}
|
|
56
46
|
|
|
57
47
|
const {
|
|
58
|
-
record,
|
|
59
|
-
href,
|
|
48
|
+
record: recordProp,
|
|
49
|
+
href: hrefProp,
|
|
50
|
+
item: itemProp,
|
|
51
|
+
pathPrefix = "/items",
|
|
60
52
|
showOwner = true,
|
|
61
53
|
showChips = true,
|
|
62
54
|
showLabel = true,
|
|
@@ -64,254 +56,280 @@ const {
|
|
|
64
56
|
class: className = "",
|
|
65
57
|
} = Astro.props;
|
|
66
58
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
// Resolve `item` (legacy) → `record` (V1) and `hrefProp` → final.
|
|
60
|
+
const item: CardRecord = (recordProp ?? itemProp) as CardRecord;
|
|
61
|
+
const slug = (item as { slug: string }).slug;
|
|
62
|
+
let detailHref: string;
|
|
63
|
+
if (typeof hrefProp === "string" && hrefProp.length > 0) {
|
|
64
|
+
detailHref = hrefProp;
|
|
65
|
+
} else {
|
|
66
|
+
const base = pathPrefix.replace(/\/$/, "");
|
|
67
|
+
detailHref = `${base}/${slug}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isProject = item.kind === "project";
|
|
71
|
+
const name = item.kind === "resource" ? (item as { title: string }).title : (item as { name: string }).name;
|
|
72
|
+
const repoHref = isProject
|
|
73
|
+
? (item as { repoUrl?: string }).repoUrl
|
|
74
|
+
: (item as { links: { github?: string } }).links?.github;
|
|
75
|
+
const homepageHref = isProject
|
|
76
|
+
? undefined
|
|
77
|
+
: (item.links as { website?: string; source?: string }).website ??
|
|
78
|
+
(item.links as { source?: string }).source;
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const platforms = (project?.platforms ?? []) as string[];
|
|
85
|
-
const healthStatus: HealthStatus | undefined = project?.health?.status;
|
|
86
|
-
const labels = record.curation?.labels ?? [];
|
|
87
|
-
// Slim (IndexProjectRecord.github) → full (ProjectRecord.github.repository).
|
|
88
|
-
const indexGh = isIndex ? (project as IndexProjectRecord).github : null;
|
|
89
|
-
const fullGh = !isIndex ? (project as ProjectRecord | null)?.github?.repository : null;
|
|
90
|
-
const stars = indexGh?.stars ?? fullGh?.stargazers_count;
|
|
91
|
-
const licenseSpdx = indexGh?.license ?? fullGh?.license?.spdx_id ?? null;
|
|
92
|
-
const pushedAt = indexGh?.pushedAt ?? fullGh?.pushed_at;
|
|
93
|
-
const githubUrl =
|
|
94
|
-
repoUrl ?? (record.links as { github?: string } | undefined)?.github;
|
|
80
|
+
// Curated = at least one of the hand-written fields is filled in.
|
|
81
|
+
const isCurated = Boolean(
|
|
82
|
+
(item as { curation?: { reviewed?: boolean } }).curation?.reviewed ||
|
|
83
|
+
(isProject && (
|
|
84
|
+
((item as { bestFor?: string[] }).bestFor?.length ?? 0) > 0 ||
|
|
85
|
+
((item as { whyListed?: string[] }).whyListed?.length ?? 0) > 0 ||
|
|
86
|
+
((item as { caveats?: string[] }).caveats?.length ?? 0) > 0
|
|
87
|
+
)),
|
|
88
|
+
);
|
|
95
89
|
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.replace(/[^a-z0-9]/gi, "")
|
|
90
|
+
// Initials for placeholder logo.
|
|
91
|
+
const initials = (name ?? "")
|
|
92
|
+
.split(/\s+/)
|
|
93
|
+
.map((w) => w[0])
|
|
94
|
+
.filter(Boolean)
|
|
102
95
|
.slice(0, 2)
|
|
96
|
+
.join("")
|
|
103
97
|
.toUpperCase();
|
|
104
98
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return { shown, overflow };
|
|
112
|
-
}
|
|
113
|
-
const stackChips = withOverflow(stacks);
|
|
114
|
-
const platformChips = withOverflow(platforms);
|
|
99
|
+
// Logo URL: explicit `logoUrl` (project only) → GitHub owner avatar
|
|
100
|
+
// → initials placeholder.
|
|
101
|
+
const { owner, repo } = getOwnerAndRepoFromRepoUrl(repoHref ?? "");
|
|
102
|
+
const logoUrl = isProject
|
|
103
|
+
? (item as { logoUrl?: string }).logoUrl ?? getOwnerAvatarUrl(owner, 80)
|
|
104
|
+
: undefined;
|
|
115
105
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
106
|
+
// Up to 4 platforms + stack chips (project only).
|
|
107
|
+
const platforms = isProject ? (item as { platforms?: string[] }).platforms ?? [] : [];
|
|
108
|
+
const visiblePlatforms = platforms.slice(0, 4);
|
|
109
|
+
const platformOverflow = platforms.length - visiblePlatforms.length;
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return "grove-badge-new";
|
|
128
|
-
case "stale":
|
|
129
|
-
case "inactive":
|
|
130
|
-
return "grove-badge-stale";
|
|
131
|
-
case "archived":
|
|
132
|
-
case "unavailable":
|
|
133
|
-
return "grove-badge-hot";
|
|
134
|
-
default:
|
|
135
|
-
return "";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
|
|
111
|
+
const allStacks = isProject
|
|
112
|
+
? [
|
|
113
|
+
(item as { stack?: string }).stack,
|
|
114
|
+
...((item as { stacks?: string[] }).stacks ?? []),
|
|
115
|
+
].filter((s): s is string => Boolean(s))
|
|
116
|
+
: [];
|
|
117
|
+
const visibleStacks = allStacks.slice(0, 3);
|
|
118
|
+
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
139
119
|
|
|
120
|
+
const stars = isProject ? (item as { github?: { stars?: number } }).github?.stars : undefined;
|
|
121
|
+
const updatedAt = isProject
|
|
122
|
+
? (item as { github?: { pushedAt?: string | null } }).github?.pushedAt
|
|
123
|
+
: (item as { publishedAt?: string | null }).publishedAt;
|
|
140
124
|
const starsLabel = formatStars(stars);
|
|
141
|
-
const updatedLabel = formatRelative(
|
|
125
|
+
const updatedLabel = formatRelative(updatedAt);
|
|
126
|
+
const license = isProject ? (item as { github?: { license?: string } }).github?.license : null;
|
|
127
|
+
const status = isProject ? (item as { health?: { status?: string } }).health?.status : undefined;
|
|
128
|
+
|
|
129
|
+
const externalHref = repoHref ?? homepageHref;
|
|
130
|
+
const statusText = status ? statusDisplay(status as Parameters<typeof statusDisplay>[0]) : "";
|
|
131
|
+
const category = (item as { category?: string }).category;
|
|
142
132
|
---
|
|
143
133
|
|
|
144
134
|
<article
|
|
145
|
-
class
|
|
146
|
-
data-record-slug={record.slug}
|
|
147
|
-
data-record-kind={record.kind}
|
|
148
|
-
data-record-category={record.category}
|
|
149
|
-
data-record-stacks={stacks.join(",")}
|
|
150
|
-
data-record-platforms={platforms.join(",")}
|
|
135
|
+
class={`card card-hover group relative flex h-full flex-col gap-3 p-4 ${className}`}
|
|
151
136
|
>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
137
|
+
{/* Top row: logo + name + (optional) status + curated mark. */}
|
|
138
|
+
<header class="relative z-10 flex items-start gap-3">
|
|
139
|
+
<a
|
|
140
|
+
href={detailHref}
|
|
141
|
+
aria-label={`Open ${name} details`}
|
|
142
|
+
class="absolute inset-0 z-0 rounded-lg"
|
|
143
|
+
></a>
|
|
144
|
+
|
|
145
|
+
{logoUrl ? (
|
|
146
|
+
<img
|
|
147
|
+
src={logoUrl}
|
|
148
|
+
alt={`${name} avatar`}
|
|
149
|
+
width="40"
|
|
150
|
+
height="40"
|
|
151
|
+
loading="lazy"
|
|
152
|
+
decoding="async"
|
|
153
|
+
class="h-10 w-10 shrink-0 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<span
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-2xs font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
|
|
159
|
+
>
|
|
160
|
+
{initials}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
165
|
+
<h3
|
|
166
|
+
class="m-0 truncate text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100"
|
|
167
|
+
>
|
|
168
|
+
{/* Name → detail page (not GitHub repo). Stacks above the
|
|
169
|
+
stretched link via z-index. */}
|
|
170
|
+
<a
|
|
171
|
+
href={detailHref}
|
|
172
|
+
class="relative z-10 text-inherit no-underline"
|
|
168
173
|
>
|
|
169
|
-
{
|
|
174
|
+
{name}
|
|
175
|
+
</a>
|
|
176
|
+
</h3>
|
|
177
|
+
{showLabel && (
|
|
178
|
+
<span class="truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
179
|
+
{showOwner && owner ? (
|
|
180
|
+
<>
|
|
181
|
+
<a
|
|
182
|
+
href={repoHref}
|
|
183
|
+
target="_blank"
|
|
184
|
+
rel="noopener noreferrer"
|
|
185
|
+
data-event="external_repo_click"
|
|
186
|
+
data-event-source="card_owner"
|
|
187
|
+
data-event-item={slug}
|
|
188
|
+
class="relative z-10 font-mono text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
189
|
+
>
|
|
190
|
+
<span class="text-ink-400 dark:text-ink-500">@</span>{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
|
|
191
|
+
</a>
|
|
192
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
193
|
+
{category}
|
|
194
|
+
{status && (
|
|
195
|
+
<>
|
|
196
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
197
|
+
<span class="text-ink-500 dark:text-ink-400">{statusText}</span>
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
</>
|
|
201
|
+
) : (
|
|
202
|
+
<>
|
|
203
|
+
{category}
|
|
204
|
+
{status && (
|
|
205
|
+
<>
|
|
206
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
207
|
+
<span class="text-ink-500 dark:text-ink-400">{statusText}</span>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
170
212
|
</span>
|
|
171
213
|
)}
|
|
172
214
|
</div>
|
|
173
215
|
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{labelDisplay(curatedLabel) ?? prettySlug(curatedLabel)}
|
|
187
|
-
</span>
|
|
188
|
-
)}
|
|
189
|
-
{healthLabel && (
|
|
190
|
-
<span class:list={["grove-badge", healthBadgeClass(healthStatus)]}>
|
|
191
|
-
{healthLabel}
|
|
192
|
-
</span>
|
|
193
|
-
)}
|
|
194
|
-
</div>
|
|
216
|
+
{isCurated && showLabel && (
|
|
217
|
+
<span
|
|
218
|
+
class="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
|
|
219
|
+
title="Hand-curated entry: bestFor, whyListed, caveats, or scores are filled in"
|
|
220
|
+
>
|
|
221
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
222
|
+
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z"/>
|
|
223
|
+
</svg>
|
|
224
|
+
curated
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
</header>
|
|
195
228
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
@{ownerRepo.owner}
|
|
205
|
-
</a>
|
|
206
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
207
|
-
<a
|
|
208
|
-
href={repoUrl}
|
|
209
|
-
target="_blank"
|
|
210
|
-
rel="noopener noreferrer"
|
|
211
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
212
|
-
>
|
|
213
|
-
{ownerRepo.repo}
|
|
214
|
-
</a>
|
|
215
|
-
</p>
|
|
216
|
-
)}
|
|
229
|
+
{/* Description */}
|
|
230
|
+
{item.description && (
|
|
231
|
+
<p
|
|
232
|
+
class="relative z-10 m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400"
|
|
233
|
+
>
|
|
234
|
+
{item.description}
|
|
235
|
+
</p>
|
|
236
|
+
)}
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
{/* Metadata row: stars · updated · license — the three signals
|
|
239
|
+
that answer "is this alive and usable?" at a glance. */}
|
|
240
|
+
{showMeta && (
|
|
241
|
+
<div class="relative z-10 flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
242
|
+
{starsLabel !== null && (
|
|
243
|
+
<span class="inline-flex items-center gap-1" title={`${stars ?? 0} stars on GitHub`}>
|
|
244
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
245
|
+
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
|
|
246
|
+
</svg>
|
|
247
|
+
<span class="font-mono">{starsLabel}</span>
|
|
248
|
+
</span>
|
|
222
249
|
)}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<span class="grove-badge" title={`Stack: ${s}`}>{s}</span>
|
|
230
|
-
))}
|
|
231
|
-
{stackChips.overflow > 0 && (
|
|
232
|
-
<span class="grove-badge" title={`${stackChips.overflow} more stacks`}>
|
|
233
|
-
+{stackChips.overflow}
|
|
250
|
+
{updatedAt && (
|
|
251
|
+
<span class="inline-flex items-center gap-1" title={`Last updated ${updatedAt}`}>
|
|
252
|
+
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-ink-400">
|
|
253
|
+
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm0 1.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm-.5 1.5a.5.5 0 0 0-1 0v3.25c0 .15.07.29.18.39l1.95 1.65a.5.5 0 0 0 .66-.76L7.5 7.55V5Z"/>
|
|
254
|
+
</svg>
|
|
255
|
+
<span class="font-mono">Updated {updatedLabel}</span>
|
|
234
256
|
</span>
|
|
235
257
|
)}
|
|
236
|
-
{
|
|
237
|
-
<span class="
|
|
238
|
-
|
|
239
|
-
{platformChips.overflow > 0 && (
|
|
240
|
-
<span
|
|
241
|
-
class="grove-badge"
|
|
242
|
-
title={`${platformChips.overflow} more platforms`}
|
|
243
|
-
>
|
|
244
|
-
+{platformChips.overflow}
|
|
258
|
+
{license && (
|
|
259
|
+
<span class="inline-flex items-center gap-1" title={`License: ${license}`}>
|
|
260
|
+
<span class="font-mono uppercase tracking-wide text-[10px]">{license}</span>
|
|
245
261
|
</span>
|
|
246
262
|
)}
|
|
247
|
-
{tags.length > 0 && tags.map((t) => <span class="grove-badge">{t}</span>)}
|
|
248
263
|
</div>
|
|
249
264
|
)}
|
|
250
265
|
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<dt class="sr-only">License</dt>
|
|
266
|
-
<dd class="m-0">{licenseSpdx}</dd>
|
|
266
|
+
{/* Platform + stack chips, pushed to the bottom. */}
|
|
267
|
+
{showChips && (
|
|
268
|
+
<div class="relative z-10 mt-auto flex flex-col gap-1.5">
|
|
269
|
+
{visiblePlatforms.length > 0 && (
|
|
270
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
271
|
+
{visiblePlatforms.map((p) => (
|
|
272
|
+
<span class="chip">
|
|
273
|
+
<Icon name={p} category="platform" size={11} />
|
|
274
|
+
<span>{p}</span>
|
|
275
|
+
</span>
|
|
276
|
+
))}
|
|
277
|
+
{platformOverflow > 0 && (
|
|
278
|
+
<span class="chip text-ink-400 dark:text-ink-500">+{platformOverflow}</span>
|
|
279
|
+
)}
|
|
267
280
|
</div>
|
|
268
281
|
)}
|
|
269
|
-
{
|
|
270
|
-
<div class="flex items-center gap-1">
|
|
271
|
-
|
|
272
|
-
|
|
282
|
+
{visibleStacks.length > 0 && (
|
|
283
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
284
|
+
{visibleStacks.map((s) => (
|
|
285
|
+
<span class="chip chip-stack">
|
|
286
|
+
<Icon name={s} category="stack" size={11} />
|
|
287
|
+
<span>{s}</span>
|
|
288
|
+
</span>
|
|
289
|
+
))}
|
|
290
|
+
{stackOverflow > 0 && (
|
|
291
|
+
<span class="chip chip-stack text-ink-400 dark:text-ink-500">+{stackOverflow}</span>
|
|
292
|
+
)}
|
|
273
293
|
</div>
|
|
274
294
|
)}
|
|
275
|
-
</
|
|
295
|
+
</div>
|
|
276
296
|
)}
|
|
277
297
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
class="grove-btn grove-btn-outline"
|
|
282
|
-
data-event="card_details_click"
|
|
283
|
-
data-event-source="item_card"
|
|
284
|
-
data-event-app={record.slug}
|
|
285
|
-
>
|
|
286
|
-
Details
|
|
287
|
-
</a>
|
|
288
|
-
{githubUrl && (
|
|
298
|
+
{/* Footer row: View repo + Details links. */}
|
|
299
|
+
<footer class="relative z-10 mt-1 flex items-center justify-between gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
300
|
+
{externalHref && (
|
|
289
301
|
<a
|
|
290
|
-
href={
|
|
302
|
+
href={externalHref}
|
|
291
303
|
target="_blank"
|
|
292
304
|
rel="noopener noreferrer"
|
|
293
|
-
|
|
294
|
-
data-event="
|
|
295
|
-
data-event-
|
|
296
|
-
|
|
305
|
+
data-event="external_repo_click"
|
|
306
|
+
data-event-source="card_view_repo"
|
|
307
|
+
data-event-item={slug}
|
|
308
|
+
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-700 transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
297
309
|
>
|
|
298
|
-
View repo
|
|
310
|
+
{repoHref ? "View repo" : "Visit source"}
|
|
311
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
312
|
+
<path d="M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Z"/>
|
|
313
|
+
<path d="M10 0a.75.75 0 0 0-.75.75v.75h-3a.75.75 0 0 0 0 1.5h3v.75A.75.75 0 0 0 10.75 4.5h2.5A.75.75 0 0 0 14 3.75v-2.5A.75.75 0 0 0 13.25.5H10Z"/>
|
|
314
|
+
</svg>
|
|
299
315
|
</a>
|
|
300
316
|
)}
|
|
301
|
-
|
|
317
|
+
<a
|
|
318
|
+
href={detailHref}
|
|
319
|
+
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
320
|
+
>
|
|
321
|
+
Details →
|
|
322
|
+
</a>
|
|
323
|
+
</footer>
|
|
302
324
|
</article>
|
|
303
325
|
|
|
304
326
|
<style>
|
|
305
|
-
.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
gap
|
|
309
|
-
height: 100%;
|
|
327
|
+
@reference "../styles.css";
|
|
328
|
+
|
|
329
|
+
.chip {
|
|
330
|
+
@apply 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 dark:border-ink-200 dark:bg-ink-950 dark:text-ink-300;
|
|
310
331
|
}
|
|
311
|
-
.
|
|
312
|
-
|
|
313
|
-
-webkit-line-clamp: 2;
|
|
314
|
-
-webkit-box-orient: vertical;
|
|
315
|
-
overflow: hidden;
|
|
332
|
+
.chip-stack {
|
|
333
|
+
@apply bg-ink-50 dark:bg-ink-900/60;
|
|
316
334
|
}
|
|
317
335
|
</style>
|