@grove-dev/astro 0.1.5 → 0.2.5
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/LICENSE +21 -0
- package/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +306 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RecordSection.astro +88 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +193 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +263 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- package/templates/default/src/pages/contributors.astro +0 -142
|
@@ -1,709 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { siteConfig } from "../../data/config";
|
|
3
|
-
import BaseLayout from "../../components/layout/BaseLayout.astro";
|
|
4
|
-
import type { OpenSourceApp } from "../../data/types";
|
|
5
|
-
import { apps } from "../../data/apps";
|
|
6
|
-
import Container from "../../components/layout/Container.astro";
|
|
7
|
-
import Icon from "../../components/icons/Icon.astro";
|
|
8
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../../lib/repo";
|
|
9
|
-
import { formatDate, formatStars } from "../../lib/format";
|
|
10
|
-
import { SCORE_DIMENSIONS, SCORE_LABELS, scoreTier, scoreLabel, SCORE_REASONING } from "../../lib/scores";
|
|
11
|
-
import { stats } from "../../data/stats";
|
|
12
|
-
|
|
13
|
-
export function getStaticPaths() {
|
|
14
|
-
return apps.map((app) => ({
|
|
15
|
-
params: { slug: app.slug },
|
|
16
|
-
props: { app },
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface Props {
|
|
21
|
-
app: OpenSourceApp;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const { app } = Astro.props;
|
|
25
|
-
|
|
26
|
-
const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
|
|
27
|
-
|
|
28
|
-
// Initials placeholder fallback
|
|
29
|
-
const initials = app.name
|
|
30
|
-
.split(/\s+/)
|
|
31
|
-
.map((w) => w[0])
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.slice(0, 2)
|
|
34
|
-
.join("")
|
|
35
|
-
.toUpperCase();
|
|
36
|
-
|
|
37
|
-
// Owner avatar URL — prefer the curated logoUrl if set, fall back to
|
|
38
|
-
// the GitHub owner avatar (matches the AppCard pattern on the home page).
|
|
39
|
-
const ownerAvatar = app.logoUrl ?? getOwnerAvatarUrl(owner, 160);
|
|
40
|
-
|
|
41
|
-
// Label for the primary label badge (new / hot / mature / stale).
|
|
42
|
-
// Stale apps override the "mature" label — see AppCard.astro for
|
|
43
|
-
// the same logic; both call sites must stay in sync.
|
|
44
|
-
const isStale = app.status === "stale";
|
|
45
|
-
const primaryLabel = isStale
|
|
46
|
-
? "stale"
|
|
47
|
-
: app.labels?.find((l) => l === "new" || l === "hot" || l === "mature") ??
|
|
48
|
-
app.labels?.[0];
|
|
49
|
-
|
|
50
|
-
const labelClass = primaryLabel === "new"
|
|
51
|
-
? "badge-new"
|
|
52
|
-
: primaryLabel === "hot"
|
|
53
|
-
? "badge-hot"
|
|
54
|
-
: primaryLabel === "mature"
|
|
55
|
-
? "badge-mature"
|
|
56
|
-
: primaryLabel === "stale"
|
|
57
|
-
? "badge-stale"
|
|
58
|
-
: "";
|
|
59
|
-
|
|
60
|
-
// All stacks (primary + secondary) for chips
|
|
61
|
-
const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
|
|
62
|
-
|
|
63
|
-
// Curation display state
|
|
64
|
-
const hasScores = app.scores && Object.values(app.scores).some((v) => typeof v === "number");
|
|
65
|
-
const hasCuration =
|
|
66
|
-
(app.bestFor && app.bestFor.length > 0) ||
|
|
67
|
-
(app.whyListed && app.whyListed.length > 0) ||
|
|
68
|
-
(app.caveats && app.caveats.length > 0);
|
|
69
|
-
|
|
70
|
-
const starsLabel = formatStars(app.stars);
|
|
71
|
-
|
|
72
|
-
const repoMeta = app.github?.repository;
|
|
73
|
-
const repoHealth = app.health;
|
|
74
|
-
const rawMonthly = app.github?.activity?.monthlyCommits ?? [];
|
|
75
|
-
const monthlyCommits = rawMonthly.map((item) =>
|
|
76
|
-
typeof item === "number" ? { month: "", commits: item } : item,
|
|
77
|
-
);
|
|
78
|
-
const maxMonthlyCommits = Math.max(1, ...monthlyCommits.map((item) => item.commits ?? 0));
|
|
79
|
-
const languages = app.github?.languages
|
|
80
|
-
? Object.entries(app.github.languages)
|
|
81
|
-
.sort((a, b) => b[1] - a[1])
|
|
82
|
-
.slice(0, 5)
|
|
83
|
-
: [];
|
|
84
|
-
const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0));
|
|
85
|
-
const files = app.github?.files ?? {};
|
|
86
|
-
const contributionSignals = [
|
|
87
|
-
{ label: "README", ok: files.readme },
|
|
88
|
-
{ label: "Contributing", ok: files.contributing },
|
|
89
|
-
{ label: "Security", ok: files.security },
|
|
90
|
-
{ label: "Issue templates", ok: files.issueTemplates },
|
|
91
|
-
{ label: "PR template", ok: files.pullRequestTemplate },
|
|
92
|
-
].filter((signal) => signal.ok !== undefined);
|
|
93
|
-
const latestRelease = app.github?.latestRelease;
|
|
94
|
-
const latestReleaseAt =
|
|
95
|
-
typeof latestRelease?.published_at === "string"
|
|
96
|
-
? latestRelease.published_at
|
|
97
|
-
: undefined;
|
|
98
|
-
const contributorsKnown = app.github?.activity?.contributorsKnown;
|
|
99
|
-
const totalCommitsKnown = app.github?.activity?.totalCommitsKnown;
|
|
100
|
-
const distributionChannels = app.distribution?.channels ?? [];
|
|
101
|
-
const distributionLabel = (type: string) => ({
|
|
102
|
-
"app-store": "App Store",
|
|
103
|
-
"play-store": "Play Store",
|
|
104
|
-
fdroid: "F-Droid",
|
|
105
|
-
"github-releases": "GitHub Releases",
|
|
106
|
-
apk: "APK",
|
|
107
|
-
testflight: "TestFlight",
|
|
108
|
-
website: "Website",
|
|
109
|
-
snapcraft: "Snapcraft",
|
|
110
|
-
flathub: "Flathub",
|
|
111
|
-
"microsoft-store": "Microsoft Store",
|
|
112
|
-
other: "Download",
|
|
113
|
-
}[type] ?? "Download");
|
|
114
|
-
|
|
115
|
-
const title = `${app.name} — ${siteConfig.name}`;
|
|
116
|
-
const description = `${app.name} — ${app.description}`;
|
|
117
|
-
|
|
118
|
-
// JSON-LD structured data for this app. Uses schema.org's
|
|
119
|
-
// SoftwareSourceCode type — that's the right fit for a source
|
|
120
|
-
// repository, more so than the generic SoftwareApplication. Helps
|
|
121
|
-
// Google show rich results (programming language, license, last
|
|
122
|
-
// commit date) on app detail pages.
|
|
123
|
-
const jsonLd: Record<string, unknown> = {
|
|
124
|
-
"@context": "https://schema.org",
|
|
125
|
-
"@type": "SoftwareSourceCode",
|
|
126
|
-
name: app.name,
|
|
127
|
-
headline: app.name,
|
|
128
|
-
description: app.description,
|
|
129
|
-
url: app.repoUrl,
|
|
130
|
-
codeRepository: app.repoUrl,
|
|
131
|
-
...(app.homepageUrl ? { sameAs: [app.repoUrl, app.homepageUrl] } : { sameAs: [app.repoUrl] }),
|
|
132
|
-
...(app.stack ? { programmingLanguage: app.stack } : {}),
|
|
133
|
-
...(app.license ? { license: app.license } : {}),
|
|
134
|
-
...(app.category ? { applicationCategory: app.category } : {}),
|
|
135
|
-
...(app.platforms && app.platforms.length > 0
|
|
136
|
-
? { operatingSystem: app.platforms.join(", ") }
|
|
137
|
-
: {}),
|
|
138
|
-
...(app.tags && app.tags.length > 0 ? { keywords: app.tags.join(", ") } : {}),
|
|
139
|
-
...(app.addedAt ? { dateCreated: app.addedAt } : {}),
|
|
140
|
-
...(app.lastCommitAt ? { dateModified: app.lastCommitAt } : {}),
|
|
141
|
-
...(app.github?.repository?.stargazers_count !== undefined
|
|
142
|
-
? { interactionStatistic: {
|
|
143
|
-
"@type": "InteractionCounter",
|
|
144
|
-
interactionType: { "@type": "LikeAction" },
|
|
145
|
-
userInteractionCount: app.github.repository.stargazers_count,
|
|
146
|
-
} }
|
|
147
|
-
: {}),
|
|
148
|
-
author: { "@type": "Organization", name: owner ?? app.name, url: app.repoUrl },
|
|
149
|
-
isAccessibleForFree: true,
|
|
150
|
-
};
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
<BaseLayout title={title} description={description} jsonLd={jsonLd}>
|
|
154
|
-
<Container>
|
|
155
|
-
<article class="pt-10 sm:pt-14">
|
|
156
|
-
{/* Breadcrumb */}
|
|
157
|
-
<nav class="text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
158
|
-
<a href="/apps" class="hover:text-ink-900 dark:hover:text-ink-100">Apps</a>
|
|
159
|
-
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
160
|
-
<span class="text-ink-700 dark:text-ink-300">{app.name}</span>
|
|
161
|
-
</nav>
|
|
162
|
-
|
|
163
|
-
{/* Header */}
|
|
164
|
-
<header class="mt-5 flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
|
|
165
|
-
<div class="flex items-start gap-4">
|
|
166
|
-
{ownerAvatar ? (
|
|
167
|
-
<img
|
|
168
|
-
src={ownerAvatar}
|
|
169
|
-
alt={`${owner} avatar`}
|
|
170
|
-
width="64"
|
|
171
|
-
height="64"
|
|
172
|
-
class="h-16 w-16 shrink-0 rounded-lg border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
173
|
-
/>
|
|
174
|
-
) : (
|
|
175
|
-
<span
|
|
176
|
-
aria-hidden="true"
|
|
177
|
-
class="flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-base font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
|
|
178
|
-
>
|
|
179
|
-
{initials}
|
|
180
|
-
</span>
|
|
181
|
-
)}
|
|
182
|
-
<div class="flex min-w-0 flex-col gap-1">
|
|
183
|
-
<h1 class="m-0 text-[1.75rem] sm:text-[2rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100">
|
|
184
|
-
{app.name}
|
|
185
|
-
</h1>
|
|
186
|
-
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
187
|
-
{owner && (
|
|
188
|
-
<a
|
|
189
|
-
href={app.repoUrl}
|
|
190
|
-
target="_blank"
|
|
191
|
-
rel="noopener noreferrer"
|
|
192
|
-
class="font-mono hover:text-ink-900 dark:hover:text-ink-100"
|
|
193
|
-
>
|
|
194
|
-
@{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
|
|
195
|
-
</a>
|
|
196
|
-
)}
|
|
197
|
-
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
198
|
-
<span>{app.category}</span>
|
|
199
|
-
{app.status && (
|
|
200
|
-
<>
|
|
201
|
-
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
202
|
-
<span class="capitalize">{app.status}</span>
|
|
203
|
-
</>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<div class="flex shrink-0 flex-wrap items-center gap-2 sm:flex-col sm:items-end sm:gap-2">
|
|
210
|
-
{primaryLabel && (
|
|
211
|
-
<span class={`badge ${labelClass} text-[10px] leading-none`}>
|
|
212
|
-
{primaryLabel}
|
|
213
|
-
</span>
|
|
214
|
-
)}
|
|
215
|
-
<a
|
|
216
|
-
href={app.repoUrl}
|
|
217
|
-
target="_blank"
|
|
218
|
-
rel="noopener noreferrer"
|
|
219
|
-
class="btn-primary"
|
|
220
|
-
>
|
|
221
|
-
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">
|
|
222
|
-
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z" />
|
|
223
|
-
</svg>
|
|
224
|
-
View on GitHub
|
|
225
|
-
</a>
|
|
226
|
-
{app.homepageUrl && (
|
|
227
|
-
<a
|
|
228
|
-
href={app.homepageUrl}
|
|
229
|
-
target="_blank"
|
|
230
|
-
rel="noopener noreferrer"
|
|
231
|
-
class="btn-outline"
|
|
232
|
-
>
|
|
233
|
-
Homepage
|
|
234
|
-
<span aria-hidden="true">↗</span>
|
|
235
|
-
</a>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
</header>
|
|
239
|
-
|
|
240
|
-
{distributionChannels.length > 0 && (
|
|
241
|
-
<section class="mt-6 flex flex-wrap items-center gap-2">
|
|
242
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
243
|
-
Download
|
|
244
|
-
</span>
|
|
245
|
-
{distributionChannels.map((channel) => (
|
|
246
|
-
<a
|
|
247
|
-
href={channel.url}
|
|
248
|
-
target="_blank"
|
|
249
|
-
rel="noopener noreferrer"
|
|
250
|
-
class="btn-outline"
|
|
251
|
-
title={channel.notes}
|
|
252
|
-
>
|
|
253
|
-
{channel.label ?? distributionLabel(channel.type)}
|
|
254
|
-
{channel.platform && (
|
|
255
|
-
<span class="text-ink-400 dark:text-ink-500">· {channel.platform}</span>
|
|
256
|
-
)}
|
|
257
|
-
</a>
|
|
258
|
-
))}
|
|
259
|
-
</section>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{/* Description */}
|
|
263
|
-
<p class="mt-6 max-w-[68ch] text-[0.9375rem] sm:text-base leading-relaxed text-ink-700 dark:text-ink-200 m-0">
|
|
264
|
-
{app.description}
|
|
265
|
-
</p>
|
|
266
|
-
|
|
267
|
-
{/* Metadata grid */}
|
|
268
|
-
<dl class="mt-8 grid grid-cols-1 gap-px overflow-hidden rounded-lg border border-ink-200 bg-ink-200 text-2xs sm:grid-cols-2 lg:grid-cols-4 dark:border-ink-800 dark:bg-ink-800">
|
|
269
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
270
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Primary stack</dt>
|
|
271
|
-
<dd class="text-ink-900 dark:text-ink-100">
|
|
272
|
-
<span class="chip chip-stack">
|
|
273
|
-
<Icon name={app.stack} category="stack" size={11} />
|
|
274
|
-
<span>{app.stack}</span>
|
|
275
|
-
</span>
|
|
276
|
-
</dd>
|
|
277
|
-
</div>
|
|
278
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
279
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Platforms</dt>
|
|
280
|
-
<dd class="flex flex-wrap gap-1 text-ink-900 dark:text-ink-100">
|
|
281
|
-
{app.platforms.map((p) => (
|
|
282
|
-
<span class="chip">
|
|
283
|
-
<Icon name={p} category="platform" size={11} />
|
|
284
|
-
<span>{p}</span>
|
|
285
|
-
</span>
|
|
286
|
-
))}
|
|
287
|
-
</dd>
|
|
288
|
-
</div>
|
|
289
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
290
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">License</dt>
|
|
291
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">{app.license ?? "—"}</dd>
|
|
292
|
-
</div>
|
|
293
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
294
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Stars</dt>
|
|
295
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
296
|
-
{starsLabel ?? "—"}
|
|
297
|
-
</dd>
|
|
298
|
-
</div>
|
|
299
|
-
{allStacks.length > 1 && (
|
|
300
|
-
<div class="flex flex-col gap-1 bg-white p-4 sm:col-span-2 dark:bg-ink-950">
|
|
301
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Also uses</dt>
|
|
302
|
-
<dd class="flex flex-wrap gap-1 text-ink-900 dark:text-ink-100">
|
|
303
|
-
{app.stacks?.map((s) => (
|
|
304
|
-
<span class="chip chip-stack">
|
|
305
|
-
<Icon name={s} category="stack" size={11} />
|
|
306
|
-
<span>{s}</span>
|
|
307
|
-
</span>
|
|
308
|
-
))}
|
|
309
|
-
</dd>
|
|
310
|
-
</div>
|
|
311
|
-
)}
|
|
312
|
-
{app.addedAt && (
|
|
313
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
314
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Added</dt>
|
|
315
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">{formatDate(app.addedAt)}</dd>
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
318
|
-
{app.lastCommitAt && (
|
|
319
|
-
<div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
|
|
320
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Last commit</dt>
|
|
321
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">{formatDate(app.lastCommitAt)}</dd>
|
|
322
|
-
</div>
|
|
323
|
-
)}
|
|
324
|
-
</dl>
|
|
325
|
-
|
|
326
|
-
{/* Decision signals */}
|
|
327
|
-
<section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
328
|
-
<div class="card p-5">
|
|
329
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
330
|
-
Repository health
|
|
331
|
-
</h2>
|
|
332
|
-
<dl class="mt-4 grid gap-3 text-sm">
|
|
333
|
-
<div class="flex items-center justify-between gap-3">
|
|
334
|
-
<dt class="text-ink-500 dark:text-ink-400">Status</dt>
|
|
335
|
-
<dd class="font-medium capitalize text-ink-900 dark:text-ink-100">
|
|
336
|
-
{repoHealth?.status ?? app.status ?? "unknown"}
|
|
337
|
-
</dd>
|
|
338
|
-
</div>
|
|
339
|
-
<div class="flex items-center justify-between gap-3">
|
|
340
|
-
<dt class="text-ink-500 dark:text-ink-400">Tier</dt>
|
|
341
|
-
<dd class="font-medium capitalize text-ink-900 dark:text-ink-100">
|
|
342
|
-
{repoHealth?.tier ?? app.tier ?? "listed"}
|
|
343
|
-
</dd>
|
|
344
|
-
</div>
|
|
345
|
-
<div class="flex items-center justify-between gap-3">
|
|
346
|
-
<dt class="text-ink-500 dark:text-ink-400">Forks</dt>
|
|
347
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
348
|
-
{repoMeta?.forks_count ?? "—"}
|
|
349
|
-
</dd>
|
|
350
|
-
</div>
|
|
351
|
-
<div class="flex items-center justify-between gap-3">
|
|
352
|
-
<dt class="text-ink-500 dark:text-ink-400">Contributors</dt>
|
|
353
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
354
|
-
{contributorsKnown ?? "—"}
|
|
355
|
-
</dd>
|
|
356
|
-
</div>
|
|
357
|
-
<div class="flex items-center justify-between gap-3">
|
|
358
|
-
<dt class="text-ink-500 dark:text-ink-400">Commits known</dt>
|
|
359
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
360
|
-
{totalCommitsKnown ?? "—"}
|
|
361
|
-
</dd>
|
|
362
|
-
</div>
|
|
363
|
-
<div class="flex items-center justify-between gap-3">
|
|
364
|
-
<dt class="text-ink-500 dark:text-ink-400">Open issues</dt>
|
|
365
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
366
|
-
{repoMeta?.open_issues_count ?? "—"}
|
|
367
|
-
</dd>
|
|
368
|
-
</div>
|
|
369
|
-
{latestReleaseAt && (
|
|
370
|
-
<div class="flex items-center justify-between gap-3">
|
|
371
|
-
<dt class="text-ink-500 dark:text-ink-400">Latest release</dt>
|
|
372
|
-
<dd class="font-mono text-ink-900 dark:text-ink-100">
|
|
373
|
-
{formatDate(latestReleaseAt)}
|
|
374
|
-
</dd>
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
</dl>
|
|
378
|
-
</div>
|
|
379
|
-
|
|
380
|
-
<div class="card p-5">
|
|
381
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
382
|
-
Activity
|
|
383
|
-
</h2>
|
|
384
|
-
{monthlyCommits.length > 0 ? (
|
|
385
|
-
<figure class="mt-4 m-0">
|
|
386
|
-
<div
|
|
387
|
-
class="flex h-28 items-end gap-1.5"
|
|
388
|
-
role="img"
|
|
389
|
-
aria-label={`Monthly commits over the last ${monthlyCommits.length} months. Peak: ${maxMonthlyCommits} commits.`}
|
|
390
|
-
>
|
|
391
|
-
{monthlyCommits.map((item) => (
|
|
392
|
-
<div class="flex min-w-0 flex-1 flex-col items-center gap-1">
|
|
393
|
-
<div
|
|
394
|
-
class="w-full rounded-sm bg-ink-700 dark:bg-ink-200"
|
|
395
|
-
style={`height: ${Math.max(8, Math.round(((item.commits ?? 0) / maxMonthlyCommits) * 96))}px`}
|
|
396
|
-
title={`${item.month || "month"}: ${item.commits ?? 0} commits`}
|
|
397
|
-
/>
|
|
398
|
-
<span class="max-w-full truncate text-[10px] text-ink-500 dark:text-ink-400">
|
|
399
|
-
{item.month ? item.month.slice(5) : item.commits}
|
|
400
|
-
</span>
|
|
401
|
-
</div>
|
|
402
|
-
))}
|
|
403
|
-
</div>
|
|
404
|
-
{/* Visually-hidden data table so screen readers can read
|
|
405
|
-
the same values sighted users see in the bars. */}
|
|
406
|
-
<figcaption class="sr-only">
|
|
407
|
-
<table>
|
|
408
|
-
<caption>Monthly commit count over the last {monthlyCommits.length} months</caption>
|
|
409
|
-
<thead>
|
|
410
|
-
<tr><th scope="col">Month</th><th scope="col">Commits</th></tr>
|
|
411
|
-
</thead>
|
|
412
|
-
<tbody>
|
|
413
|
-
{monthlyCommits.map((item) => (
|
|
414
|
-
<tr>
|
|
415
|
-
<th scope="row">{item.month || "Unknown"}</th>
|
|
416
|
-
<td>{item.commits ?? 0}</td>
|
|
417
|
-
</tr>
|
|
418
|
-
))}
|
|
419
|
-
</tbody>
|
|
420
|
-
</table>
|
|
421
|
-
</figcaption>
|
|
422
|
-
</figure>
|
|
423
|
-
) : (
|
|
424
|
-
<div class="mt-4 space-y-1.5">
|
|
425
|
-
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
426
|
-
Monthly commit history is not yet populated for this app.
|
|
427
|
-
</p>
|
|
428
|
-
<p class="m-0 text-2xs text-ink-400 dark:text-ink-500">
|
|
429
|
-
Activity data refreshes daily via
|
|
430
|
-
<a
|
|
431
|
-
href={`${stats.originalRepo}/blob/main/.github/workflows/sync-github-metadata.yml`}
|
|
432
|
-
target="_blank"
|
|
433
|
-
rel="noopener noreferrer"
|
|
434
|
-
class="text-ink-700 hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
435
|
-
>sync-github-metadata</a
|
|
436
|
-
>. Last commit and stars are already in sync.
|
|
437
|
-
</p>
|
|
438
|
-
</div>
|
|
439
|
-
)}
|
|
440
|
-
</div>
|
|
441
|
-
|
|
442
|
-
<div class="card p-5">
|
|
443
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
444
|
-
Contribution readiness
|
|
445
|
-
</h2>
|
|
446
|
-
{contributionSignals.length > 0 ? (
|
|
447
|
-
<ul class="mt-4 m-0 grid list-none gap-2 p-0 text-sm">
|
|
448
|
-
{contributionSignals.map((signal) => (
|
|
449
|
-
<li class="flex items-center justify-between gap-3">
|
|
450
|
-
<span class="text-ink-700 dark:text-ink-200">{signal.label}</span>
|
|
451
|
-
<span class={signal.ok ? "text-emerald-700 dark:text-emerald-400" : "text-ink-400 dark:text-ink-600"}>
|
|
452
|
-
{signal.ok ? "Found" : "Missing"}
|
|
453
|
-
</span>
|
|
454
|
-
</li>
|
|
455
|
-
))}
|
|
456
|
-
</ul>
|
|
457
|
-
) : (
|
|
458
|
-
<p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
459
|
-
Contribution files have not been synced yet.
|
|
460
|
-
</p>
|
|
461
|
-
)}
|
|
462
|
-
</div>
|
|
463
|
-
</section>
|
|
464
|
-
|
|
465
|
-
{languages.length > 0 && (
|
|
466
|
-
<section class="mt-4 card p-5">
|
|
467
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
468
|
-
Language mix
|
|
469
|
-
</h2>
|
|
470
|
-
<dl class="mt-4 grid gap-2 text-sm">
|
|
471
|
-
{languages.map(([language, bytes]) => {
|
|
472
|
-
const pct = Math.round((bytes / totalLanguageBytes) * 100);
|
|
473
|
-
return (
|
|
474
|
-
<div class="grid grid-cols-[8rem_minmax(0,1fr)_3rem] items-center gap-3">
|
|
475
|
-
<dt class="truncate text-ink-700 dark:text-ink-200">{language}</dt>
|
|
476
|
-
<dd class="h-2 overflow-hidden rounded-sm bg-ink-200 dark:bg-ink-800">
|
|
477
|
-
<div class="h-full bg-ink-700 dark:bg-ink-200" style={`width: ${pct}%`} />
|
|
478
|
-
</dd>
|
|
479
|
-
<dd class="text-right font-mono text-2xs text-ink-500 dark:text-ink-400">{pct}%</dd>
|
|
480
|
-
</div>
|
|
481
|
-
);
|
|
482
|
-
})}
|
|
483
|
-
</dl>
|
|
484
|
-
</section>
|
|
485
|
-
)}
|
|
486
|
-
|
|
487
|
-
{/* Curation block — scores + Best for / Why listed / Caveats */}
|
|
488
|
-
{(hasScores || hasCuration) && (
|
|
489
|
-
<section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-5">
|
|
490
|
-
{hasScores && (
|
|
491
|
-
<div class="card p-5 lg:col-span-3">
|
|
492
|
-
<header class="mb-4 flex items-baseline justify-between gap-2">
|
|
493
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
494
|
-
Scores
|
|
495
|
-
</h2>
|
|
496
|
-
{app.curation?.reviewed ? (
|
|
497
|
-
<span 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">
|
|
498
|
-
Curator reviewed
|
|
499
|
-
</span>
|
|
500
|
-
) : app.curation?.by ? (
|
|
501
|
-
<span class="text-2xs text-ink-500 dark:text-ink-400">
|
|
502
|
-
Auto-curated · {app.curation.by}
|
|
503
|
-
</span>
|
|
504
|
-
) : (
|
|
505
|
-
<span class="text-2xs text-ink-500 dark:text-ink-400">
|
|
506
|
-
Auto-curated
|
|
507
|
-
</span>
|
|
508
|
-
)}
|
|
509
|
-
</header>
|
|
510
|
-
<dl class="flex flex-col gap-2.5 text-sm">
|
|
511
|
-
{SCORE_DIMENSIONS.map((dim) => {
|
|
512
|
-
const v = app.scores![dim];
|
|
513
|
-
const tier = typeof v === "number" ? scoreTier(v) : 0;
|
|
514
|
-
const isOverall = dim === "overall";
|
|
515
|
-
return (
|
|
516
|
-
<div class="flex items-center gap-3">
|
|
517
|
-
<dt
|
|
518
|
-
class={`w-24 shrink-0 ${
|
|
519
|
-
isOverall
|
|
520
|
-
? "font-semibold text-ink-900 dark:text-ink-100"
|
|
521
|
-
: "text-ink-700 dark:text-ink-200"
|
|
522
|
-
}`}
|
|
523
|
-
>
|
|
524
|
-
{SCORE_LABELS[dim]}
|
|
525
|
-
</dt>
|
|
526
|
-
<dd
|
|
527
|
-
class="flex h-2 flex-1 gap-px"
|
|
528
|
-
aria-label={`${SCORE_LABELS[dim]} ${scoreLabel(v)} of 100`}
|
|
529
|
-
>
|
|
530
|
-
{[0, 1, 2, 3].map((i) => (
|
|
531
|
-
<div
|
|
532
|
-
class={`flex-1 rounded-sm ${
|
|
533
|
-
i < tier
|
|
534
|
-
? isOverall
|
|
535
|
-
? "bg-ink-700 dark:bg-ink-200"
|
|
536
|
-
: "bg-ink-500 dark:bg-ink-400"
|
|
537
|
-
: "bg-ink-200 dark:bg-ink-800"
|
|
538
|
-
}`}
|
|
539
|
-
/>
|
|
540
|
-
))}
|
|
541
|
-
</dd>
|
|
542
|
-
<dd
|
|
543
|
-
class={`w-7 shrink-0 text-right font-mono tabular-nums ${
|
|
544
|
-
isOverall
|
|
545
|
-
? "font-semibold text-ink-900 dark:text-ink-100"
|
|
546
|
-
: "text-ink-700 dark:text-ink-300"
|
|
547
|
-
}`}
|
|
548
|
-
>
|
|
549
|
-
{scoreLabel(v)}
|
|
550
|
-
</dd>
|
|
551
|
-
</div>
|
|
552
|
-
);
|
|
553
|
-
})}
|
|
554
|
-
</dl>
|
|
555
|
-
<dl class="mt-5 flex flex-col gap-1.5 border-t border-ink-200 pt-4 text-2xs dark:border-ink-800">
|
|
556
|
-
{SCORE_DIMENSIONS.map((dim) => (
|
|
557
|
-
<div class="flex items-baseline gap-2">
|
|
558
|
-
<dt class="w-24 shrink-0 font-medium text-ink-500 dark:text-ink-400">
|
|
559
|
-
{SCORE_LABELS[dim]}
|
|
560
|
-
</dt>
|
|
561
|
-
<dd class="text-ink-600 dark:text-ink-300">
|
|
562
|
-
{SCORE_REASONING[dim]}
|
|
563
|
-
</dd>
|
|
564
|
-
</div>
|
|
565
|
-
))}
|
|
566
|
-
</dl>
|
|
567
|
-
</div>
|
|
568
|
-
)}
|
|
569
|
-
|
|
570
|
-
{hasCuration && (
|
|
571
|
-
<div class="card p-5 lg:col-span-2">
|
|
572
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
573
|
-
Curation
|
|
574
|
-
</h2>
|
|
575
|
-
<div class="mt-4 flex flex-col gap-4 text-sm">
|
|
576
|
-
{app.bestFor && app.bestFor.length > 0 && (
|
|
577
|
-
<div>
|
|
578
|
-
<div class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
579
|
-
Best for
|
|
580
|
-
</div>
|
|
581
|
-
<ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
|
|
582
|
-
{app.bestFor.map((s) => (
|
|
583
|
-
<li class="flex items-baseline gap-2">
|
|
584
|
-
<span class="text-ink-400 dark:text-ink-500" aria-hidden="true">·</span>
|
|
585
|
-
<span>{s}</span>
|
|
586
|
-
</li>
|
|
587
|
-
))}
|
|
588
|
-
</ul>
|
|
589
|
-
</div>
|
|
590
|
-
)}
|
|
591
|
-
{app.whyListed && app.whyListed.length > 0 && (
|
|
592
|
-
<div>
|
|
593
|
-
<div class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
594
|
-
Why listed
|
|
595
|
-
</div>
|
|
596
|
-
<ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
|
|
597
|
-
{app.whyListed.map((s) => (
|
|
598
|
-
<li class="flex items-baseline gap-2">
|
|
599
|
-
<span class="text-ink-400 dark:text-ink-500" aria-hidden="true">·</span>
|
|
600
|
-
<span>{s}</span>
|
|
601
|
-
</li>
|
|
602
|
-
))}
|
|
603
|
-
</ul>
|
|
604
|
-
</div>
|
|
605
|
-
)}
|
|
606
|
-
{app.caveats && app.caveats.length > 0 && (
|
|
607
|
-
<div>
|
|
608
|
-
<div class="text-2xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
|
609
|
-
Caveats
|
|
610
|
-
</div>
|
|
611
|
-
<ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
|
|
612
|
-
{app.caveats.map((s) => (
|
|
613
|
-
<li class="flex items-baseline gap-2">
|
|
614
|
-
<span class="text-amber-500 dark:text-amber-400" aria-hidden="true">·</span>
|
|
615
|
-
<span>{s}</span>
|
|
616
|
-
</li>
|
|
617
|
-
))}
|
|
618
|
-
</ul>
|
|
619
|
-
</div>
|
|
620
|
-
)}
|
|
621
|
-
</div>
|
|
622
|
-
</div>
|
|
623
|
-
)}
|
|
624
|
-
</section>
|
|
625
|
-
)}
|
|
626
|
-
|
|
627
|
-
{/* Project shape — secondary curation metadata */}
|
|
628
|
-
{(app.projectType || app.stateManagement || app.backend || app.architecture || app.difficulty || app.codebaseSize) && (
|
|
629
|
-
<section class="mt-4 card p-5">
|
|
630
|
-
<h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
631
|
-
Project shape
|
|
632
|
-
</h2>
|
|
633
|
-
<dl class="mt-4 grid grid-cols-1 gap-x-6 gap-y-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
|
634
|
-
{app.projectType && (
|
|
635
|
-
<div>
|
|
636
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Type</dt>
|
|
637
|
-
<dd class="mt-0.5 text-ink-700 dark:text-ink-200">
|
|
638
|
-
{app.projectType === "real-app" ? "Real app" :
|
|
639
|
-
app.projectType === "production" ? "Production" :
|
|
640
|
-
app.projectType === "reference" ? "Reference" :
|
|
641
|
-
app.projectType === "demo" ? "Demo" :
|
|
642
|
-
app.projectType === "template" ? "Template" : app.projectType}
|
|
643
|
-
</dd>
|
|
644
|
-
</div>
|
|
645
|
-
)}
|
|
646
|
-
{app.stateManagement && (
|
|
647
|
-
<div>
|
|
648
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">State management</dt>
|
|
649
|
-
<dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.stateManagement}</dd>
|
|
650
|
-
</div>
|
|
651
|
-
)}
|
|
652
|
-
{app.backend && (
|
|
653
|
-
<div>
|
|
654
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Backend</dt>
|
|
655
|
-
<dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.backend}</dd>
|
|
656
|
-
</div>
|
|
657
|
-
)}
|
|
658
|
-
{app.architecture && (
|
|
659
|
-
<div>
|
|
660
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Architecture</dt>
|
|
661
|
-
<dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.architecture}</dd>
|
|
662
|
-
</div>
|
|
663
|
-
)}
|
|
664
|
-
{app.difficulty && (
|
|
665
|
-
<div>
|
|
666
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Difficulty</dt>
|
|
667
|
-
<dd class="mt-0.5 capitalize text-ink-700 dark:text-ink-200">{app.difficulty}</dd>
|
|
668
|
-
</div>
|
|
669
|
-
)}
|
|
670
|
-
{app.codebaseSize && (
|
|
671
|
-
<div>
|
|
672
|
-
<dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Codebase size</dt>
|
|
673
|
-
<dd class="mt-0.5 capitalize text-ink-700 dark:text-ink-200">{app.codebaseSize}</dd>
|
|
674
|
-
</div>
|
|
675
|
-
)}
|
|
676
|
-
</dl>
|
|
677
|
-
</section>
|
|
678
|
-
)}
|
|
679
|
-
|
|
680
|
-
{/* Tags */}
|
|
681
|
-
{app.tags && app.tags.length > 0 && (
|
|
682
|
-
<div class="mt-4 flex flex-wrap items-center gap-1.5">
|
|
683
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Tags</span>
|
|
684
|
-
{app.tags.map((t) => (
|
|
685
|
-
<a
|
|
686
|
-
href={`/apps?q=${encodeURIComponent(t)}`}
|
|
687
|
-
class="chip hover:border-ink-300 dark:hover:border-ink-700"
|
|
688
|
-
>
|
|
689
|
-
{t}
|
|
690
|
-
</a>
|
|
691
|
-
))}
|
|
692
|
-
</div>
|
|
693
|
-
)}
|
|
694
|
-
|
|
695
|
-
{/* Related section removed — this is a code reference, not an
|
|
696
|
-
e-commerce page. Use the Apps list to find more by stack. */}
|
|
697
|
-
</article>
|
|
698
|
-
</Container>
|
|
699
|
-
<style>
|
|
700
|
-
@reference "../../styles/global.css";
|
|
701
|
-
|
|
702
|
-
.chip {
|
|
703
|
-
@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-800 dark:bg-ink-950 dark:text-ink-300;
|
|
704
|
-
}
|
|
705
|
-
.chip-stack {
|
|
706
|
-
@apply bg-ink-50 dark:bg-ink-900/60;
|
|
707
|
-
}
|
|
708
|
-
</style>
|
|
709
|
-
</BaseLayout>
|