@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,691 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Detail page — one record at a time.
|
|
4
|
-
*
|
|
5
|
-
* Layout (top to bottom):
|
|
6
|
-
* 1. Breadcrumb — back to the directory index
|
|
7
|
-
* 2. AppCard-style header — logo + name + owner + description + CTAs
|
|
8
|
-
* 3. ScoreBars — 5-dimension + overall score block
|
|
9
|
-
* 4. BestFor / WhyListed / Caveats — 3-column curation grid
|
|
10
|
-
* 5. GitHub signals dl — stars, forks, pushed, license, language
|
|
11
|
-
* 6. Stack + Platform chips — Icon (with fallback to text)
|
|
12
|
-
* 7. Distribution channels — npm/crates/docker/etc. (when present)
|
|
13
|
-
* 8. Content markdown — `record.content` (rendered as raw HTML
|
|
14
|
-
* body) when present
|
|
15
|
-
* 9. JSON-LD SoftwareSourceCode — for SEO + AI crawlers
|
|
16
|
-
*
|
|
17
|
-
* Generic across the 3 record kinds (project / resource / entity) —
|
|
18
|
-
* non-project fields simply render nothing. The URL is dynamic on
|
|
19
|
-
* both `[slug]` (blueprint route, e.g. `projects`/`resources`/
|
|
20
|
-
* `entities`) and `[recordSlug]` (the record slug), so a single page
|
|
21
|
-
* file serves every blueprint and every record.
|
|
22
|
-
*/
|
|
23
|
-
import siteConfig from "../../../data/generated/site-config.json";
|
|
24
|
-
import {
|
|
25
|
-
fullRecords,
|
|
26
|
-
projectBySlug,
|
|
27
|
-
resourceBySlug,
|
|
28
|
-
entityBySlug,
|
|
29
|
-
indexSlug,
|
|
30
|
-
getContentHtml,
|
|
31
|
-
} from "../../data/records";
|
|
32
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
33
|
-
import Icon from "@grove-dev/astro/components/Icon.astro";
|
|
34
|
-
import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
|
|
35
|
-
import CurationGrid from "@grove-dev/astro/components/CurationGrid.astro";
|
|
36
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "@grove-dev/astro";
|
|
37
|
-
import { formatRelative, formatStars } from "@grove-dev/astro";
|
|
38
|
-
import { statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
|
|
39
|
-
import type { LabelId } from "@grove-dev/astro";
|
|
40
|
-
import type { ProjectRecord } from "@grove-dev/core";
|
|
41
|
-
|
|
42
|
-
export function getStaticPaths() {
|
|
43
|
-
// Walk every record and emit a path for it. The directory slug
|
|
44
|
-
// is read from the static site-config import at the top of this
|
|
45
|
-
// file (Vite resolves the JSON at build time). This template
|
|
46
|
-
// ships with the `project-directory` blueprint, so the slug is
|
|
47
|
-
// always `projects`; swapping in a different blueprint means
|
|
48
|
-
// changing the blueprint config that drives this value.
|
|
49
|
-
const dirSlug = siteConfig.blueprintConfig?.routeSlug ?? "projects";
|
|
50
|
-
return fullRecords.map((record) => ({
|
|
51
|
-
params: { slug: dirSlug, recordSlug: record.slug },
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const slug = Astro.params.slug ?? indexSlug();
|
|
56
|
-
const recordSlug = Astro.params.recordSlug ?? "";
|
|
57
|
-
const project = projectBySlug(recordSlug);
|
|
58
|
-
const resource = resourceBySlug(recordSlug);
|
|
59
|
-
const entity = entityBySlug(recordSlug);
|
|
60
|
-
const record = project ?? resource ?? entity;
|
|
61
|
-
|
|
62
|
-
if (!record) {
|
|
63
|
-
return Astro.redirect(`/${slug}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const isProject = record.kind === "project";
|
|
67
|
-
const proj: ProjectRecord | null = project ?? null;
|
|
68
|
-
|
|
69
|
-
const name =
|
|
70
|
-
record.kind === "resource" ? (record as { title: string }).title : (record as { name: string }).name;
|
|
71
|
-
|
|
72
|
-
const title = `${name} — ${siteConfig.name}`;
|
|
73
|
-
const description = record.description ?? `${name} on ${siteConfig.name}`;
|
|
74
|
-
const itemSingular = siteConfig.blueprintConfig?.labelSingular ?? record.kind;
|
|
75
|
-
|
|
76
|
-
const repoUrl = proj?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
|
|
77
|
-
const homepageUrl = record.links?.website ?? "";
|
|
78
|
-
const logoUrl = proj?.logoUrl;
|
|
79
|
-
const stacks = proj?.stacks ?? (proj?.stack ? [proj.stack] : []);
|
|
80
|
-
const platforms = proj?.platforms ?? [];
|
|
81
|
-
const github = proj?.github?.repository;
|
|
82
|
-
const stars = github?.stargazers_count ?? 0;
|
|
83
|
-
const forks = github?.forks_count ?? 0;
|
|
84
|
-
const language = github?.language ?? null;
|
|
85
|
-
const licenseSpdx = github?.license?.spdx_id ?? null;
|
|
86
|
-
const pushedAt = github?.pushed_at ?? null;
|
|
87
|
-
const starsLabel = formatStars(stars);
|
|
88
|
-
const pushedLabel = formatRelative(pushedAt);
|
|
89
|
-
|
|
90
|
-
const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
|
|
91
|
-
const avatarSrc = logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
|
|
92
|
-
const initials = (name || "").replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase();
|
|
93
|
-
|
|
94
|
-
const bestFor = proj?.bestFor ?? [];
|
|
95
|
-
const whyListed = proj?.whyListed ?? [];
|
|
96
|
-
const caveats = proj?.caveats ?? [];
|
|
97
|
-
const distribution = proj?.distribution?.channels ?? [];
|
|
98
|
-
const scores = proj?.scores ?? null;
|
|
99
|
-
const curationLabels = record.curation?.labels ?? [];
|
|
100
|
-
const tags = record.tags ?? [];
|
|
101
|
-
const healthStatus = proj?.health?.status;
|
|
102
|
-
const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
|
|
103
|
-
|
|
104
|
-
// Map each known label to a static Tailwind class string so every
|
|
105
|
-
// utility is visible to Tailwind's source scanner. Unknown/legacy
|
|
106
|
-
// labels use the neutral secondary badge treatment.
|
|
107
|
-
const labelClassMap: Record<LabelId, string> = {
|
|
108
|
-
new: "!bg-emerald-50 !text-emerald-700 dark:!bg-emerald-950/80 dark:!text-emerald-300",
|
|
109
|
-
hot: "!bg-orange-50 !text-orange-800 dark:!bg-orange-950/80 dark:!text-orange-300",
|
|
110
|
-
mature: "!bg-blue-50 !text-blue-700 dark:!bg-blue-950/80 dark:!text-blue-300",
|
|
111
|
-
featured: "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",
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// ── Optional content markdown (record.content is a path under
|
|
115
|
-
// content/records/<slug>.md) ────────────────────────────────────
|
|
116
|
-
// The read + marked.parse + sanitize-html pipeline lives in
|
|
117
|
-
// `data/records.ts` and runs once at module load. The page just
|
|
118
|
-
// looks up the pre-sanitized HTML by record slug; no node:fs
|
|
119
|
-
// import needed here.
|
|
120
|
-
const contentHtml: string | null = isProject ? getContentHtml(recordSlug) : null;
|
|
121
|
-
|
|
122
|
-
// ── Detail-page decision-signal data (5 sections) ────────────
|
|
123
|
-
// Hoist out of the template body so the JSX stays free of
|
|
124
|
-
// complex TypeScript type assertions.
|
|
125
|
-
type MonthlyCommit = number | { month: string; commits: number };
|
|
126
|
-
type ProjectExtras = {
|
|
127
|
-
github?: {
|
|
128
|
-
activity?: { monthlyCommits?: MonthlyCommit[] };
|
|
129
|
-
files?: Record<string, boolean>;
|
|
130
|
-
languages?: Record<string, number>;
|
|
131
|
-
};
|
|
132
|
-
health?: { status?: string; tier?: string };
|
|
133
|
-
};
|
|
134
|
-
const extras = record as ProjectExtras;
|
|
135
|
-
const monthlyCommits = (extras.github?.activity?.monthlyCommits ?? []).map(
|
|
136
|
-
(it): { month: string; commits: number } =>
|
|
137
|
-
typeof it === "number" ? { month: "", commits: it } : { month: it.month ?? "", commits: it.commits ?? 0 },
|
|
138
|
-
);
|
|
139
|
-
const maxMonthlyCommits = Math.max(1, ...monthlyCommits.map((i) => i.commits));
|
|
140
|
-
const contributionFiles = extras.github?.files ?? {};
|
|
141
|
-
const contributionLabels: Record<string, string> = {
|
|
142
|
-
readme: "README",
|
|
143
|
-
contributing: "Contributing",
|
|
144
|
-
security: "Security",
|
|
145
|
-
issueTemplates: "Issue templates",
|
|
146
|
-
pullRequestTemplate: "PR template",
|
|
147
|
-
};
|
|
148
|
-
const contributionSignals = (Object.keys(contributionLabels) as Array<keyof typeof contributionLabels>)
|
|
149
|
-
.map((k) => ({ key: k, label: contributionLabels[k], ok: contributionFiles[k] }))
|
|
150
|
-
.filter((s) => s.ok !== undefined);
|
|
151
|
-
const languages = extras.github?.languages
|
|
152
|
-
? Object.entries(extras.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
|
|
153
|
-
: [];
|
|
154
|
-
const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, b]) => sum + b, 0));
|
|
155
|
-
|
|
156
|
-
// ── Kind-aware JSON-LD ─────────────────────────────────────────
|
|
157
|
-
// Mirrors the openapps field set (10+ fields for projects):
|
|
158
|
-
// name, headline, description, url, codeRepository, sameAs,
|
|
159
|
-
// programmingLanguage, license, applicationCategory,
|
|
160
|
-
// operatingSystem, keywords, dateCreated, dateModified,
|
|
161
|
-
// interactionStatistic, author, isAccessibleForFree.
|
|
162
|
-
let jsonLd: Record<string, unknown>;
|
|
163
|
-
if (isProject && proj) {
|
|
164
|
-
const sameAs = [repoUrl, homepageUrl].filter((value): value is string =>
|
|
165
|
-
typeof value === "string" && value.length > 0,
|
|
166
|
-
);
|
|
167
|
-
const keywords = (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined;
|
|
168
|
-
const operatingSystem = (proj.platforms ?? []).length > 0
|
|
169
|
-
? (proj.platforms ?? []).join(", ")
|
|
170
|
-
: undefined;
|
|
171
|
-
const applicationCategory = (record.category ?? "") || undefined;
|
|
172
|
-
const dateCreated = (record as { curation?: { reviewedAt?: string } }).curation?.reviewedAt;
|
|
173
|
-
const dateModified = pushedAt ?? dateCreated;
|
|
174
|
-
const interactionStatistic = stars > 0
|
|
175
|
-
? {
|
|
176
|
-
"@type": "InteractionCounter",
|
|
177
|
-
interactionType: { "@type": "LikeAction" },
|
|
178
|
-
userInteractionCount: stars,
|
|
179
|
-
}
|
|
180
|
-
: undefined;
|
|
181
|
-
jsonLd = {
|
|
182
|
-
"@context": "https://schema.org",
|
|
183
|
-
"@type": "SoftwareSourceCode",
|
|
184
|
-
name,
|
|
185
|
-
headline: name,
|
|
186
|
-
description: record.description ?? undefined,
|
|
187
|
-
url: repoUrl || homepageUrl || undefined,
|
|
188
|
-
codeRepository: repoUrl || undefined,
|
|
189
|
-
sameAs: sameAs.length > 0 ? sameAs : undefined,
|
|
190
|
-
programmingLanguage: language ?? undefined,
|
|
191
|
-
license: licenseSpdx ?? undefined,
|
|
192
|
-
applicationCategory,
|
|
193
|
-
operatingSystem,
|
|
194
|
-
keywords,
|
|
195
|
-
dateCreated: dateCreated ?? undefined,
|
|
196
|
-
dateModified: dateModified ?? undefined,
|
|
197
|
-
interactionStatistic,
|
|
198
|
-
author: ownerRepo
|
|
199
|
-
? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
|
|
200
|
-
: undefined,
|
|
201
|
-
isAccessibleForFree: true,
|
|
202
|
-
};
|
|
203
|
-
} else if (record.kind === "resource") {
|
|
204
|
-
const resourceSchemaType: Record<string, string> = {
|
|
205
|
-
article: "Article",
|
|
206
|
-
book: "Book",
|
|
207
|
-
course: "Course",
|
|
208
|
-
podcast: "PodcastSeries",
|
|
209
|
-
video: "VideoObject",
|
|
210
|
-
};
|
|
211
|
-
jsonLd = {
|
|
212
|
-
"@context": "https://schema.org",
|
|
213
|
-
"@type": resourceSchemaType[record.type] ?? "CreativeWork",
|
|
214
|
-
name,
|
|
215
|
-
headline: name,
|
|
216
|
-
description: record.description || undefined,
|
|
217
|
-
url: homepageUrl || undefined,
|
|
218
|
-
author: record.author ? { "@type": "Person", name: record.author } : undefined,
|
|
219
|
-
datePublished: record.publishedAt || undefined,
|
|
220
|
-
keywords: (record.tags ?? []).length > 0 ? (record.tags ?? []).join(", ") : undefined,
|
|
221
|
-
isAccessibleForFree: true,
|
|
222
|
-
};
|
|
223
|
-
} else if (entity) {
|
|
224
|
-
jsonLd = {
|
|
225
|
-
"@context": "https://schema.org",
|
|
226
|
-
"@type": entity.type === "person" ? "Person" : "Organization",
|
|
227
|
-
name,
|
|
228
|
-
headline: name,
|
|
229
|
-
description: entity.description || undefined,
|
|
230
|
-
url: homepageUrl || undefined,
|
|
231
|
-
foundingDate: entity.founded || undefined,
|
|
232
|
-
location: entity.location || undefined,
|
|
233
|
-
keywords: (entity.tags ?? []).length > 0 ? (entity.tags ?? []).join(", ") : undefined,
|
|
234
|
-
};
|
|
235
|
-
} else {
|
|
236
|
-
jsonLd = {};
|
|
237
|
-
}
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
<BaseLayout
|
|
241
|
-
title={title}
|
|
242
|
-
description={description}
|
|
243
|
-
site={siteConfig}
|
|
244
|
-
type="article"
|
|
245
|
-
jsonLd={jsonLd}
|
|
246
|
-
submitHref="/submit"
|
|
247
|
-
submitLabel={`Submit ${name}`}
|
|
248
|
-
>
|
|
249
|
-
<article class="py-12 sm:py-16">
|
|
250
|
-
<div class="mx-auto w-full max-w-wide px-5 sm:px-7">
|
|
251
|
-
<nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
252
|
-
<a href={`/${slug}`} class="hover:text-ink-900 dark:hover:text-ink-100 capitalize">
|
|
253
|
-
{slug}
|
|
254
|
-
</a>
|
|
255
|
-
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
256
|
-
<span class="text-ink-700 dark:text-ink-300">{name}</span>
|
|
257
|
-
</nav>
|
|
258
|
-
|
|
259
|
-
{/* ── AppCard-style header ──────────────────────────────── */}
|
|
260
|
-
<header class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
|
|
261
|
-
<div class="flex-shrink-0">
|
|
262
|
-
{avatarSrc ? (
|
|
263
|
-
<img
|
|
264
|
-
src={avatarSrc}
|
|
265
|
-
alt=""
|
|
266
|
-
width="64"
|
|
267
|
-
height="64"
|
|
268
|
-
loading="lazy"
|
|
269
|
-
decoding="async"
|
|
270
|
-
class="h-16 w-16 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950"
|
|
271
|
-
/>
|
|
272
|
-
) : (
|
|
273
|
-
<span
|
|
274
|
-
aria-hidden="true"
|
|
275
|
-
class="inline-flex h-16 w-16 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-2xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
|
|
276
|
-
>
|
|
277
|
-
{initials || "·"}
|
|
278
|
-
</span>
|
|
279
|
-
)}
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<div class="min-w-0 flex-1">
|
|
283
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
284
|
-
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
285
|
-
{record.kind}
|
|
286
|
-
</span>
|
|
287
|
-
{record.category && (
|
|
288
|
-
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
289
|
-
{record.category}
|
|
290
|
-
</span>
|
|
291
|
-
)}
|
|
292
|
-
{curationLabels.map((l) => (
|
|
293
|
-
<span 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 ${labelClassMap[l as LabelId] ?? ""}`} title={`Curated: ${labelDisplay(l) ?? l}`}>
|
|
294
|
-
{labelDisplay(l) ?? prettySlug(l)}
|
|
295
|
-
</span>
|
|
296
|
-
))}
|
|
297
|
-
{record.curation?.reviewed && (
|
|
298
|
-
<span
|
|
299
|
-
class="inline-flex items-center gap-1 rounded border border-emerald-300/60 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300"
|
|
300
|
-
title="A human curator reviewed this record. (No reviewer name is shown — the framework is reviewer-agnostic; consumers can wire the curation.by field into their own badge component.)"
|
|
301
|
-
>
|
|
302
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
303
|
-
<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" />
|
|
304
|
-
</svg>
|
|
305
|
-
Curator reviewed
|
|
306
|
-
</span>
|
|
307
|
-
)}
|
|
308
|
-
{healthLabel && (
|
|
309
|
-
<span 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 !bg-amber-50 !text-amber-800 dark:!bg-amber-950/80 dark:!text-amber-300">{healthLabel}</span>
|
|
310
|
-
)}
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
<h1 class="m-0 mt-2 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
314
|
-
{name}
|
|
315
|
-
</h1>
|
|
316
|
-
|
|
317
|
-
{ownerRepo && (
|
|
318
|
-
<p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
|
|
319
|
-
<a
|
|
320
|
-
href={`https://github.com/${ownerRepo.owner}`}
|
|
321
|
-
target="_blank"
|
|
322
|
-
rel="noopener noreferrer"
|
|
323
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
324
|
-
>
|
|
325
|
-
@{ownerRepo.owner}
|
|
326
|
-
</a>
|
|
327
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
328
|
-
<a
|
|
329
|
-
href={repoUrl}
|
|
330
|
-
target="_blank"
|
|
331
|
-
rel="noopener noreferrer"
|
|
332
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
333
|
-
>
|
|
334
|
-
{ownerRepo.repo}
|
|
335
|
-
</a>
|
|
336
|
-
</p>
|
|
337
|
-
)}
|
|
338
|
-
|
|
339
|
-
{record.description && (
|
|
340
|
-
<p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
341
|
-
{record.description}
|
|
342
|
-
</p>
|
|
343
|
-
)}
|
|
344
|
-
|
|
345
|
-
<div class="mt-5 flex flex-wrap gap-2">
|
|
346
|
-
{repoUrl && (
|
|
347
|
-
<a
|
|
348
|
-
href={repoUrl}
|
|
349
|
-
target="_blank"
|
|
350
|
-
rel="noopener noreferrer"
|
|
351
|
-
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"
|
|
352
|
-
>
|
|
353
|
-
View on GitHub →
|
|
354
|
-
</a>
|
|
355
|
-
)}
|
|
356
|
-
{homepageUrl && (
|
|
357
|
-
<a
|
|
358
|
-
href={homepageUrl}
|
|
359
|
-
target="_blank"
|
|
360
|
-
rel="noopener noreferrer"
|
|
361
|
-
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"
|
|
362
|
-
>
|
|
363
|
-
Homepage
|
|
364
|
-
</a>
|
|
365
|
-
)}
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
</header>
|
|
369
|
-
|
|
370
|
-
{/* ── Score bars ───────────────────────────────────────── */}
|
|
371
|
-
{proj && (
|
|
372
|
-
<section class="mt-8 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-6" aria-labelledby="scores-heading">
|
|
373
|
-
<h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
374
|
-
Scores
|
|
375
|
-
</h2>
|
|
376
|
-
<p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
|
|
377
|
-
A five-dimension read of how likely this project is to satisfy a curious developer.
|
|
378
|
-
</p>
|
|
379
|
-
<div class="mt-4">
|
|
380
|
-
<ScoreBars scores={scores} />
|
|
381
|
-
</div>
|
|
382
|
-
</section>
|
|
383
|
-
)}
|
|
384
|
-
|
|
385
|
-
{/* ── BestFor / WhyListed / Caveats grid ──────────────── */}
|
|
386
|
-
{isProject && (
|
|
387
|
-
<CurationGrid
|
|
388
|
-
bestFor={bestFor}
|
|
389
|
-
whyListed={whyListed}
|
|
390
|
-
caveats={caveats}
|
|
391
|
-
class="mt-8"
|
|
392
|
-
/>
|
|
393
|
-
)}
|
|
394
|
-
|
|
395
|
-
{/* ── GitHub signals dl ────────────────────────────────── */}
|
|
396
|
-
{isProject && github && (
|
|
397
|
-
<section class="mt-8 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-6" aria-labelledby="signals-heading">
|
|
398
|
-
<h2 id="signals-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
399
|
-
GitHub signals
|
|
400
|
-
</h2>
|
|
401
|
-
<dl
|
|
402
|
-
class="mt-4 grid grid-cols-2 gap-x-6 gap-y-3 text-sm sm:grid-cols-3"
|
|
403
|
-
aria-label="Repository signals"
|
|
404
|
-
>
|
|
405
|
-
<div>
|
|
406
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
407
|
-
Stars
|
|
408
|
-
</dt>
|
|
409
|
-
<dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
|
|
410
|
-
{starsLabel ?? "—"}
|
|
411
|
-
</dd>
|
|
412
|
-
</div>
|
|
413
|
-
<div>
|
|
414
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
415
|
-
Forks
|
|
416
|
-
</dt>
|
|
417
|
-
<dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
|
|
418
|
-
{forks}
|
|
419
|
-
</dd>
|
|
420
|
-
</div>
|
|
421
|
-
<div>
|
|
422
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
423
|
-
Last push
|
|
424
|
-
</dt>
|
|
425
|
-
<dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
|
|
426
|
-
{pushedLabel}
|
|
427
|
-
</dd>
|
|
428
|
-
</div>
|
|
429
|
-
{language && (
|
|
430
|
-
<div>
|
|
431
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
432
|
-
Language
|
|
433
|
-
</dt>
|
|
434
|
-
<dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{language}</dd>
|
|
435
|
-
</div>
|
|
436
|
-
)}
|
|
437
|
-
{licenseSpdx && (
|
|
438
|
-
<div>
|
|
439
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
440
|
-
License
|
|
441
|
-
</dt>
|
|
442
|
-
<dd class="m-0 mt-0.5 text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
|
|
443
|
-
</div>
|
|
444
|
-
)}
|
|
445
|
-
{pushedAt && (
|
|
446
|
-
<div>
|
|
447
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
448
|
-
Updated
|
|
449
|
-
</dt>
|
|
450
|
-
<dd class="m-0 mt-0.5 font-mono text-ink-900 dark:text-ink-100">
|
|
451
|
-
{pushedAt.slice(0, 10)}
|
|
452
|
-
</dd>
|
|
453
|
-
</div>
|
|
454
|
-
)}
|
|
455
|
-
</dl>
|
|
456
|
-
</section>
|
|
457
|
-
)}
|
|
458
|
-
|
|
459
|
-
{/* ── Decision signals (3-col) ──────────────────────────── */}
|
|
460
|
-
{isProject && github && (
|
|
461
|
-
<section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3" aria-label="Decision signals">
|
|
462
|
-
{/* Repository health card */}
|
|
463
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
464
|
-
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
465
|
-
Repository health
|
|
466
|
-
</h3>
|
|
467
|
-
<dl class="mt-4 grid gap-3 text-sm">
|
|
468
|
-
<div class="flex items-center justify-between gap-3">
|
|
469
|
-
<dt class="text-ink-500 dark:text-ink-400">Status</dt>
|
|
470
|
-
<dd class="font-medium text-ink-900 dark:text-ink-100">
|
|
471
|
-
{healthLabel ?? "—"}
|
|
472
|
-
</dd>
|
|
473
|
-
</div>
|
|
474
|
-
<div class="flex items-center justify-between gap-3">
|
|
475
|
-
<dt class="text-ink-500 dark:text-ink-400">Stars</dt>
|
|
476
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
477
|
-
{starsLabel ?? "—"}
|
|
478
|
-
</dd>
|
|
479
|
-
</div>
|
|
480
|
-
<div class="flex items-center justify-between gap-3">
|
|
481
|
-
<dt class="text-ink-500 dark:text-ink-400">Forks</dt>
|
|
482
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
483
|
-
{forks}
|
|
484
|
-
</dd>
|
|
485
|
-
</div>
|
|
486
|
-
<div class="flex items-center justify-between gap-3">
|
|
487
|
-
<dt class="text-ink-500 dark:text-ink-400">Open issues</dt>
|
|
488
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
489
|
-
{github?.open_issues_count ?? "—"}
|
|
490
|
-
</dd>
|
|
491
|
-
</div>
|
|
492
|
-
{licenseSpdx && (
|
|
493
|
-
<div class="flex items-center justify-between gap-3">
|
|
494
|
-
<dt class="text-ink-500 dark:text-ink-400">License</dt>
|
|
495
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">{licenseSpdx}</dd>
|
|
496
|
-
</div>
|
|
497
|
-
)}
|
|
498
|
-
</dl>
|
|
499
|
-
</div>
|
|
500
|
-
|
|
501
|
-
{/* Activity bar chart */}
|
|
502
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
503
|
-
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
504
|
-
Activity
|
|
505
|
-
</h3>
|
|
506
|
-
{monthlyCommits.length > 0 ? (
|
|
507
|
-
<figure class="mt-4 m-0">
|
|
508
|
-
<div
|
|
509
|
-
class="flex h-28 items-end gap-1.5"
|
|
510
|
-
role="img"
|
|
511
|
-
aria-label="Monthly commits over the last 12 months"
|
|
512
|
-
>
|
|
513
|
-
{monthlyCommits.map((item) => (
|
|
514
|
-
<div class="flex min-w-0 flex-1 flex-col items-center gap-1">
|
|
515
|
-
<div
|
|
516
|
-
class="w-full rounded-sm bg-ink-700 dark:bg-ink-200"
|
|
517
|
-
style={`height: ${Math.max(8, Math.round((item.commits / maxMonthlyCommits) * 96))}px`}
|
|
518
|
-
title={`${item.month || "month"}: ${item.commits} commits`}
|
|
519
|
-
/>
|
|
520
|
-
<span class="max-w-full truncate text-[10px] text-ink-500 dark:text-ink-400">
|
|
521
|
-
{item.month ? item.month.slice(5) : item.commits}
|
|
522
|
-
</span>
|
|
523
|
-
</div>
|
|
524
|
-
))}
|
|
525
|
-
</div>
|
|
526
|
-
<figcaption class="sr-only">
|
|
527
|
-
Monthly commit count over the last 12 months
|
|
528
|
-
</figcaption>
|
|
529
|
-
</figure>
|
|
530
|
-
) : (
|
|
531
|
-
<p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
532
|
-
Monthly commit history is not yet populated for this {itemSingular}.
|
|
533
|
-
</p>
|
|
534
|
-
)}
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
{/* Contribution readiness chips */}
|
|
538
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
539
|
-
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
540
|
-
Contribution readiness
|
|
541
|
-
</h3>
|
|
542
|
-
{contributionSignals.length > 0 ? (
|
|
543
|
-
<ul class="mt-4 m-0 grid list-none gap-2 p-0 text-sm">
|
|
544
|
-
{contributionSignals.map((signal) => (
|
|
545
|
-
<li class="flex items-center justify-between gap-3">
|
|
546
|
-
<span class="text-ink-700 dark:text-ink-200">{signal.label}</span>
|
|
547
|
-
<span class={signal.ok ? "text-emerald-700 dark:text-emerald-400" : "text-ink-400 dark:text-ink-600"}>
|
|
548
|
-
{signal.ok ? "Found" : "Missing"}
|
|
549
|
-
</span>
|
|
550
|
-
</li>
|
|
551
|
-
))}
|
|
552
|
-
</ul>
|
|
553
|
-
) : (
|
|
554
|
-
<p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
555
|
-
Contribution files have not been synced yet.
|
|
556
|
-
</p>
|
|
557
|
-
)}
|
|
558
|
-
</div>
|
|
559
|
-
</section>
|
|
560
|
-
)}
|
|
561
|
-
|
|
562
|
-
{/* ── Language mix ─────────────────────────────────────── */}
|
|
563
|
-
{isProject && languages.length > 0 && (
|
|
564
|
-
<section class="mt-4 rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
565
|
-
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
566
|
-
Language mix
|
|
567
|
-
</h3>
|
|
568
|
-
<dl class="mt-4 grid gap-2 text-sm">
|
|
569
|
-
{languages.map(([language, bytes]) => {
|
|
570
|
-
const pct = Math.round((bytes / totalLanguageBytes) * 100);
|
|
571
|
-
return (
|
|
572
|
-
<div class="grid grid-cols-[8rem_minmax(0,1fr)_3rem] items-center gap-3">
|
|
573
|
-
<dt class="truncate text-ink-700 dark:text-ink-200">{language}</dt>
|
|
574
|
-
<dd class="h-2 overflow-hidden rounded-sm bg-ink-200 dark:bg-ink-800">
|
|
575
|
-
<div class="h-full bg-ink-700 dark:bg-ink-200" style={`width: ${pct}%`} />
|
|
576
|
-
</dd>
|
|
577
|
-
<dd class="text-right font-mono text-2xs text-ink-500 dark:text-ink-400">{pct}%</dd>
|
|
578
|
-
</div>
|
|
579
|
-
);
|
|
580
|
-
})}
|
|
581
|
-
</dl>
|
|
582
|
-
</section>
|
|
583
|
-
)}
|
|
584
|
-
|
|
585
|
-
{/* ── Stack + Platform chips ───────────────────────────── */}
|
|
586
|
-
{isProject && (stacks.length > 0 || platforms.length > 0) && (
|
|
587
|
-
<section class="mt-8" aria-labelledby="stacks-heading">
|
|
588
|
-
<h2 id="stacks-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
589
|
-
Stack & platforms
|
|
590
|
-
</h2>
|
|
591
|
-
<div class="mt-4 flex flex-col gap-3">
|
|
592
|
-
{stacks.length > 0 && (
|
|
593
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
594
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
595
|
-
Stack
|
|
596
|
-
</span>
|
|
597
|
-
{stacks.map((s) => (
|
|
598
|
-
<span 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 items-center gap-1.5">
|
|
599
|
-
<Icon name={s} category="stack" size={14} />
|
|
600
|
-
{s}
|
|
601
|
-
</span>
|
|
602
|
-
))}
|
|
603
|
-
</div>
|
|
604
|
-
)}
|
|
605
|
-
{platforms.length > 0 && (
|
|
606
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
607
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
608
|
-
Platform
|
|
609
|
-
</span>
|
|
610
|
-
{platforms.map((p) => (
|
|
611
|
-
<span 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 items-center gap-1.5">
|
|
612
|
-
<Icon name={p} category="platform" size={14} />
|
|
613
|
-
{p}
|
|
614
|
-
</span>
|
|
615
|
-
))}
|
|
616
|
-
</div>
|
|
617
|
-
)}
|
|
618
|
-
</div>
|
|
619
|
-
</section>
|
|
620
|
-
)}
|
|
621
|
-
|
|
622
|
-
{/* ── Tags ─────────────────────────────────────────────── */}
|
|
623
|
-
{tags.length > 0 && (
|
|
624
|
-
<section class="mt-8" aria-labelledby="tags-heading">
|
|
625
|
-
<h2 id="tags-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
626
|
-
Tags
|
|
627
|
-
</h2>
|
|
628
|
-
<ul class="mt-4 m-0 flex list-none flex-wrap gap-1.5 p-0">
|
|
629
|
-
{tags.map((t) => (
|
|
630
|
-
<li>
|
|
631
|
-
<a
|
|
632
|
-
href={`/${slug}?q=${encodeURIComponent(t)}`}
|
|
633
|
-
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 items-center gap-1.5"
|
|
634
|
-
>
|
|
635
|
-
{t}
|
|
636
|
-
</a>
|
|
637
|
-
</li>
|
|
638
|
-
))}
|
|
639
|
-
</ul>
|
|
640
|
-
</section>
|
|
641
|
-
)}
|
|
642
|
-
|
|
643
|
-
{/* ── Distribution channels ────────────────────────────── */}
|
|
644
|
-
{distribution.length > 0 && (
|
|
645
|
-
<section class="mt-8" aria-labelledby="dist-heading">
|
|
646
|
-
<h2 id="dist-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
647
|
-
Distribution channels
|
|
648
|
-
</h2>
|
|
649
|
-
<ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
|
|
650
|
-
{distribution.map((c) => (
|
|
651
|
-
<li class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-wrap items-baseline justify-between gap-2 p-3">
|
|
652
|
-
<div class="flex flex-col">
|
|
653
|
-
<span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
|
|
654
|
-
{c.label ?? c.type}
|
|
655
|
-
{c.verified && (
|
|
656
|
-
<span class="ml-2 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 !bg-emerald-50 !text-emerald-700 dark:!bg-emerald-950/80 dark:!text-emerald-300">Verified</span>
|
|
657
|
-
)}
|
|
658
|
-
</span>
|
|
659
|
-
{c.notes && (
|
|
660
|
-
<span class="text-xs text-ink-500 dark:text-ink-400">{c.notes}</span>
|
|
661
|
-
)}
|
|
662
|
-
</div>
|
|
663
|
-
<a
|
|
664
|
-
href={c.url}
|
|
665
|
-
target="_blank"
|
|
666
|
-
rel="noopener noreferrer"
|
|
667
|
-
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"
|
|
668
|
-
>
|
|
669
|
-
{c.platform ?? c.type} →
|
|
670
|
-
</a>
|
|
671
|
-
</li>
|
|
672
|
-
))}
|
|
673
|
-
</ul>
|
|
674
|
-
</section>
|
|
675
|
-
)}
|
|
676
|
-
|
|
677
|
-
{/* ── Content markdown ─────────────────────────────────── */}
|
|
678
|
-
{contentHtml && (
|
|
679
|
-
<section class="mt-8" aria-labelledby="content-heading">
|
|
680
|
-
<h2 id="content-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
681
|
-
Notes
|
|
682
|
-
</h2>
|
|
683
|
-
<div
|
|
684
|
-
class="grove-prose mt-4"
|
|
685
|
-
set:html={contentHtml}
|
|
686
|
-
/>
|
|
687
|
-
</section>
|
|
688
|
-
)}
|
|
689
|
-
</div>
|
|
690
|
-
</article>
|
|
691
|
-
</BaseLayout>
|